chore: auto-commit (286 archivos)

- .claude/agents/fn-orquestador/SKILL.md
- .claude/commands/fn_claude.md
- .claude/rules/INDEX.md
- .claude/rules/cpp_apps.md
- .claude/rules/ids_naming.md
- CHANGELOG.md
- apps/dag_engine/README.md
- apps/dag_engine/api.go
- apps/dag_engine/dags_migrated/example.yaml
- apps/dag_engine/dags_migrated/example_lineage_tracking.yaml
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 16:33:22 +02:00
parent d6175964e4
commit 212875ed0d
290 changed files with 12703 additions and 19778 deletions
Submodule cpp/apps/altsnap_jitter_test deleted from 6e52b658a3
-22
View File
@@ -1,22 +0,0 @@
add_imgui_app(chart_demo
main.cpp
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
# fps_overlay vive en fn_framework
)
# --- E2E tests (opt-in via -DFN_BUILD_TESTS=ON) ---
if(FN_BUILD_TESTS)
add_imgui_app(chart_demo_tests
main.cpp
tests/chart_demo_tests.cpp
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
)
# Excludes int main() from main.cpp so the test target provides its own.
target_compile_definitions(chart_demo_tests PRIVATE FN_TEST_BUILD)
endif()
-60
View File
@@ -1,60 +0,0 @@
---
name: chart_demo
lang: cpp
domain: viz
description: "Demo ImGui de primitivos viz del registry: line_plot, scatter_plot, bar_chart, heatmap. Cada chart en su propia tab del TabBar. Usado como showcase y como build gate de las funciones viz/."
tags: [imgui, demo, charts, viz, showcase]
uses_functions:
- line_plot_cpp_viz
- scatter_plot_cpp_viz
- bar_chart_cpp_viz
- heatmap_cpp_viz
# logger, app_menubar viven en fn_framework — no se listan aqui
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/chart_demo"
repo_url: ""
---
## Que hace
App de una sola ventana con cuatro tabs (Line / Scatter / Bar / Heatmap) que
renderiza datos sinteticos para mostrar el aspecto y la API de los primitivos
viz del registry. Sirve como:
- **Showcase visual** de las funciones viz existentes — al añadir una nueva
primitiva, anadir su tab aqui es la forma natural de probar el binding.
- **Build gate**: si una de las funciones rompe API, esta app deja de
compilar y lo cazamos sin tener que tocar `registry_dashboard` o
`graph_explorer`.
## Estructura
`main.cpp` (~93 lineas):
- `init_data()` — genera arrays sinteticos una vez (estado modulo).
- `render()` — DockSpaceOverViewport + TabBar con 4 tabs, cada una invoca
un primitivo del registry.
- `main()``fn::run_app(...)` con AppConfig estandar (titulo, tamaño,
about, log).
## Build
```bash
# Linux
cd cpp && cmake -B build/linux -S . && cmake --build build/linux --target chart_demo
# Windows (cross-compile)
cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w64.cmake \
&& cmake --build build/windows --target chart_demo
```
## Decisiones
- `viewports = true` (default de `fn::run_app`): las ventanas se pueden
arrastrar fuera del main window.
- `init_gl_loader = false`: solo usa ImGui/ImPlot, sin gl* directo.
- Sin persistencia propia (no abre BD).
- `log: file_path = "chart_demo.log"` con nivel Debug — el `init_data`
emite info+debug para verificar que el logger funciona.
-89
View File
@@ -1,89 +0,0 @@
#include "app_base.h"
#include "imgui.h"
#include "implot.h"
#include "viz/line_plot.h"
#include "viz/scatter_plot.h"
#include "viz/bar_chart.h"
#include "viz/heatmap.h"
#include "core/app_menubar.h"
#include "core/logger.h"
#include <cmath>
#include <vector>
// Generate sample data
static constexpr int N = 500;
static float xs[N], ys_sin[N], ys_cos[N];
static float scatter_x[200], scatter_y[200];
static const char* bar_labels[] = {"Go", "Python", "Bash", "TypeScript", "C++"};
static float bar_values[] = {201.0f, 202.0f, 38.0f, 80.0f, 5.0f};
static float heat_data[10 * 10];
static bool data_initialized = false;
static void init_data() {
if (data_initialized) return;
fn_log::log_info("init_data: generando %d puntos sin/cos, 200 scatter, 10x10 heatmap", N);
for (int i = 0; i < N; i++) {
xs[i] = static_cast<float>(i) * 0.02f;
ys_sin[i] = sinf(xs[i]);
ys_cos[i] = cosf(xs[i]);
}
for (int i = 0; i < 200; i++) {
scatter_x[i] = static_cast<float>(rand()) / RAND_MAX * 10.0f;
scatter_y[i] = scatter_x[i] * 0.5f + (static_cast<float>(rand()) / RAND_MAX - 0.5f) * 3.0f;
}
for (int i = 0; i < 100; i++) {
int r = i / 10, c = i % 10;
heat_data[i] = sinf(r * 0.5f) * cosf(c * 0.5f);
}
data_initialized = true;
fn_log::log_debug("init_data: ok");
}
void render() {
init_data();
if (ImGui::Begin("fn_registry — Chart Demo")) {
if (ImGui::BeginTabBar("##charts")) {
if (ImGui::BeginTabItem("Line Plot")) {
ImGui::Text("sin(x) — %d points", N);
line_plot("Sine Wave", xs, ys_sin, N);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Scatter Plot")) {
ImGui::Text("y = 0.5x + noise — 200 points");
scatter_plot("Scatter Data", scatter_x, scatter_y, 200);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Bar Chart")) {
ImGui::Text("Functions per language in fn_registry");
bar_chart("Registry Languages", bar_labels, bar_values, 5);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Heatmap")) {
ImGui::Text("sin(r) * cos(c) — 10x10 matrix");
heatmap("Correlation Matrix", heat_data, 10, 10, -1.0f, 1.0f);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
ImGui::End();
}
#ifndef FN_TEST_BUILD
int main() {
return fn::run_app({
.title = "fn_registry — Chart Demo",
.width = 1400,
.height = 900,
.about = {.name = "chart demo",
.version = "0.2.0",
.description = "Demo de primitivos viz: line, scatter, bar, heatmap. AppConfig estandar + multi-viewport."},
.log = {.file_path = "chart_demo.log",
.level = static_cast<int>(fn_log::Level::Debug)}
}, render);
}
#endif
@@ -1,41 +0,0 @@
// E2E tests for chart_demo — Dear ImGui Test Engine.
// Built only when -DFN_BUILD_TESTS=ON. The same main.cpp from chart_demo is
// compiled here with FN_TEST_BUILD defined so its int main() is excluded and
// only render() is reused.
#include "app_base.h"
#include "imgui.h"
#include "imgui_te_engine.h"
#include "imgui_te_context.h"
void render(); // defined in chart_demo/main.cpp
static void register_tests(ImGuiTestEngine* e) {
ImGuiTest* t = nullptr;
// Smoke test: the main window appears and is non-empty.
t = IM_REGISTER_TEST(e, "chart_demo", "smoke_window_visible");
t->TestFunc = [](ImGuiTestContext* ctx) {
ctx->SetRef("fn_registry \xe2\x80\x94 Chart Demo"); // em-dash
IM_CHECK(ctx->WindowInfo("").ID != 0);
};
// Cycle through all four tabs. Test engine fails the test if any tab item
// is not found or cannot be activated — that is our implicit assertion.
t = IM_REGISTER_TEST(e, "chart_demo", "tabs_cycle_all");
t->TestFunc = [](ImGuiTestContext* ctx) {
ctx->SetRef("fn_registry \xe2\x80\x94 Chart Demo");
ctx->ItemClick("##charts/Line Plot");
ctx->ItemClick("##charts/Scatter Plot");
ctx->ItemClick("##charts/Bar Chart");
ctx->ItemClick("##charts/Heatmap");
};
}
int main() {
fn::AppConfig cfg{};
cfg.title = "chart_demo_tests";
cfg.width = 1280;
cfg.height = 800;
return fn::run_app_test(cfg, render, register_tests);
}
Submodule cpp/apps/dag_engine_ui deleted from aec22ba594
Submodule cpp/apps/engine_smoke deleted from bed33856e7
-110
View File
@@ -1,110 +0,0 @@
add_imgui_app(primitives_gallery
main.cpp
capture.cpp
demo.cpp
demos_core.cpp
demos_viz.cpp
demos_graph.cpp
demos_graph_styles.cpp
demos_gfx.cpp
demos_3d.cpp
demos_text_editor.cpp
demos_gl_texture.cpp
demos_extras.cpp
demos_mesh.cpp
# animation primitives (issue 0031)
demos_animation.cpp
${CMAKE_SOURCE_DIR}/functions/core/tween_curves.cpp
${CMAKE_SOURCE_DIR}/functions/core/bezier_editor.cpp
${CMAKE_SOURCE_DIR}/functions/core/timeline.cpp
demos_sql.cpp
demos_scientific.cpp
# text_editor + file_watcher (issue 0025) + file_poll_diff pure (issue 0045)
${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_poll_diff.cpp
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit/TextEditor.cpp
# sql_workbench (issue 0032) + sql_parse pure (issue 0045)
${CMAKE_SOURCE_DIR}/functions/core/sql_workbench.cpp
${CMAKE_SOURCE_DIR}/functions/core/sql_parse.cpp
# Core primitives demoed (tokens vive en fn_framework)
${CMAKE_SOURCE_DIR}/functions/core/fullscreen_window.cpp
${CMAKE_SOURCE_DIR}/functions/core/page_header.cpp
${CMAKE_SOURCE_DIR}/functions/core/dashboard_panel.cpp
${CMAKE_SOURCE_DIR}/functions/core/badge.cpp
${CMAKE_SOURCE_DIR}/functions/core/empty_state.cpp
${CMAKE_SOURCE_DIR}/functions/core/button.cpp
${CMAKE_SOURCE_DIR}/functions/core/icon_button.cpp
${CMAKE_SOURCE_DIR}/functions/core/toolbar.cpp
${CMAKE_SOURCE_DIR}/functions/core/modal_dialog.cpp
${CMAKE_SOURCE_DIR}/functions/core/text_input.cpp
${CMAKE_SOURCE_DIR}/functions/core/select.cpp
${CMAKE_SOURCE_DIR}/functions/core/toast.cpp
${CMAKE_SOURCE_DIR}/functions/core/tree_view.cpp
${CMAKE_SOURCE_DIR}/functions/core/process_runner.cpp
${CMAKE_SOURCE_DIR}/functions/core/process_state_machine.cpp
# Viz primitives demoed
${CMAKE_SOURCE_DIR}/functions/viz/kpi_card.cpp
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/pie_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/histogram.cpp
${CMAKE_SOURCE_DIR}/functions/viz/sparkline.cpp
${CMAKE_SOURCE_DIR}/functions/viz/candlestick.cpp
${CMAKE_SOURCE_DIR}/functions/viz/gauge.cpp
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
${CMAKE_SOURCE_DIR}/functions/viz/table_view.cpp
# 3D viz primitives (issue 0028, ImPlot3D)
${CMAKE_SOURCE_DIR}/functions/viz/surface_plot_3d.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_3d.cpp
# Scientific viz (issue 0034)
${CMAKE_SOURCE_DIR}/functions/viz/treemap.cpp
${CMAKE_SOURCE_DIR}/functions/viz/sankey.cpp
${CMAKE_SOURCE_DIR}/functions/viz/chord.cpp
${CMAKE_SOURCE_DIR}/functions/viz/contour.cpp
${CMAKE_SOURCE_DIR}/functions/viz/voronoi.cpp
# Graph stack (instanced GPU + Barnes-Hut + spatial hash)
${CMAKE_SOURCE_DIR}/functions/viz/graph_types.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_renderer.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_icons.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_force_layout.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_force_layout_gpu.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_layouts.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_viewport.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_viewport_selection.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_labels.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_labels_select.cpp
${CMAKE_SOURCE_DIR}/functions/core/graph_spatial_hash.cpp
# GL loader (Linux no-op, Windows wglGetProcAddress)
${CMAKE_SOURCE_DIR}/functions/gfx/gl_loader.cpp
# Shader stack (shader_canvas demo)
${CMAKE_SOURCE_DIR}/functions/gfx/gl_shader.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/gl_framebuffer.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/fullscreen_quad.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/shader_canvas.cpp
# gl_texture_load (issue 0026) + stb_image
${CMAKE_SOURCE_DIR}/functions/gfx/gl_texture_load.cpp
${CMAKE_SOURCE_DIR}/vendor/stb/stb_image_impl.cpp
# mesh_viewer stack (issue 0029)
${CMAKE_SOURCE_DIR}/functions/gfx/mesh_obj_load.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/mesh_gpu.cpp
${CMAKE_SOURCE_DIR}/functions/core/orbit_camera.cpp
${CMAKE_SOURCE_DIR}/functions/viz/mesh_viewer.cpp
)
target_include_directories(primitives_gallery PRIVATE
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit
${CMAKE_SOURCE_DIR}/vendor/stb
)
# SQLite (sql_workbench) — alias provisto por cpp/CMakeLists.txt:
# system on Linux, vendored amalgamation on Windows cross-compile.
target_link_libraries(primitives_gallery PRIVATE SQLite::SQLite3)
if(WIN32)
target_link_libraries(primitives_gallery PRIVATE opengl32)
endif()
if(WIN32)
set_target_properties(primitives_gallery PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
-159
View File
@@ -1,159 +0,0 @@
# primitives_gallery
Catalogo visual interactivo de los primitivos UI del registry (`cpp/functions/core` y `cpp/functions/viz`). Un solo ejecutable con sidebar izquierdo + panel derecho que renderiza la demo del primitivo seleccionado con todas sus variantes y un snippet de codigo.
## Rol
| Funcion | Como lo cumple |
|---|---|
| Smoke test visual | Abrir la gallery tras un cambio en tokens / componentes; si algo se ve raro, lo cazas en segundos. |
| Documentacion viva | Cada demo muestra el componente trabajando + el snippet exacto. Mas rapido que leer los `.md`. |
| Build gate | Esta en el CMake principal (`cpp/CMakeLists.txt`). Si un primitivo rompe API, la gallery no compila => CI rojo. |
| Sandbox de prototipos | Datos sinteticos, sin backend; ideal para iterar un primitivo nuevo sin tocar el dashboard. |
## Build & run
```bash
# Linux
cmake --build cpp/build/linux --target primitives_gallery -j$(nproc)
./cpp/build/linux/apps/primitives_gallery/primitives_gallery
# Windows (cross-compile)
cmake --build cpp/build/windows --target primitives_gallery -j$(nproc)
# binario: cpp/build/windows/apps/primitives_gallery/primitives_gallery.exe
```
No se conecta a `sqlite_api` ni a ningun backend. Datos sinteticos generados in-memory.
## Demos disponibles
### Core
| Demo | Primitivo | Que muestra |
|---|---|---|
| button | `button_cpp_core` | 4 variantes x 3 sizes |
| icon_button | `icon_button_cpp_core` | Glyphs comunes con tooltip |
| toolbar | `toolbar_cpp_core` | Dos grupos con separador vertical |
| modal_dialog | `modal_dialog_cpp_core` | Boton que abre modal con form |
| text_input | `text_input_cpp_core` | 3 inputs con placeholder |
| select | `select_cpp_core` | Dropdown con y sin `(none)` |
| toast + inbox | `toast_cpp_core` (v1.1) | 4 botones que disparan toasts + campana con badge |
| tree_view | `tree_view_cpp_core` | Arbol fake de proyectos -> apps |
| badge | `badge_cpp_core` | 6 variantes semanticas |
| empty_state | `empty_state_cpp_core` | Lista vacia con icono + cta |
| page_header | `page_header_cpp_core` | Header con toolbar a la derecha |
| dashboard_panel | `dashboard_panel_cpp_core` | Panel con titulo y borde |
| kpi_card | `kpi_card_cpp_viz` (v1.2) | Grid 1x4 con sparklines y delta |
### Viz
| Demo | Primitivo | Que muestra |
|---|---|---|
| bar_chart | `bar_chart_cpp_viz` (v1.2) | Labels que caben + labels rotados 45 |
| pie_chart | `pie_chart_cpp_viz` (v1.1) | Pie + donut con tooltip por slice |
| line_plot | `line_plot_cpp_viz` (v1.1) | Serie sintetica `sin(t) + ruido` |
| scatter_plot | `scatter_plot_cpp_viz` (v1.1) | 120 puntos con correlacion |
| histogram | `histogram_cpp_viz` (v1.1) | 300 muestras gaussianas |
| sparkline | `sparkline_cpp_viz` | Trending up / down / flat |
| graph_viewport | `graph_viewport_cpp_viz` | **Ver seccion abajo** |
## Demo `graph_viewport` (en detalle)
Pipeline completo de visualizacion de grafos con instanced GPU rendering:
- `graph_renderer_cpp_viz` (1 draw call para todos los nodos via `glDrawArraysInstanced`)
- `graph_force_layout_cpp_viz` (Barnes-Hut, paso de simulacion por frame)
- `graph_spatial_hash_cpp_core` (hit-testing O(1) bajo el cursor)
- `graph_viewport_cpp_viz` (widget que orquesta los anteriores con pan/zoom/select)
### Controles
| Control | Rango | Efecto |
|---|---|---|
| `Nodes` | 100 20 000 | Numero de nodos a generar |
| `Clusters` | 2 16 | Numero de comunidades (cada una con su color) |
| `Repulsion` | 100 20 000 | Fuerza repulsiva entre todos los nodos. Mas alto => grafo mas extendido y energia mayor. |
| `Attraction` | 0.001 0.5 | Constante del muelle de las aristas. Mas alto => clusters mas compactos. |
| `Gravity` | 0.0 0.05 | Tiron hacia (0,0). Util para evitar drift cuando subes mucho la repulsion. |
| `Regenerate` | boton | Regenera el grafo con los valores actuales de Nodes/Clusters. |
| `Pause / Resume layout` | boton | Para o reanuda la simulacion force-directed. |
| `Fit view` | boton | Encuadra la camara al bounding box del grafo con 10% de padding. |
Los tres sliders de fuerzas se leen cada frame y se inyectan en `ForceLayoutConfig`, asi que cambiar un valor durante el layout en marcha re-calibra el sistema al instante.
### Stats line (sin vibracion)
Una sola linea fija — sin secciones condicionales que cambien la altura del panel:
```
nodes=N edges=E energy=X fps=F | hover=#id cN sel=#id
```
`hover` y `sel` muestran `-` cuando no hay nada seleccionado para mantener el ancho/alto estable; antes una fila condicional desplazaba el viewport en cada hover.
### Interaccion con el viewport
| Gesto | Accion |
|---|---|
| Drag con boton izquierdo en zona vacia | Pan de camara |
| Wheel | Zoom (limites 0.01x 50x) |
| Drag sobre nodo | Mueve el nodo (lo `pin`ea durante el drag) |
| Click sobre nodo | Selecciona (`s_state.selected_node`) |
| Hover sobre nodo | Resaltado + `s_state.hovered_node` poblado |
### Datos sinteticos
`generate_synthetic_graph(N, K)` reparte N nodos en K clusters dispuestos en circulo, con ~3 aristas intra-cluster por nodo y un 5% adicional de aristas inter-cluster. Paleta de 8 colores ABGR. Posiciones iniciales con dispersion gaussiana de 80 px alrededor del centroide del cluster — el force layout las reordena en pocos frames.
### Performance esperada
| Nodes | FPS objetivo (RTX 30xx, viewport 800x460) | Notas |
|---|---|---|
| 1 000 | 60 (vsync) | Caso comun; layout converge < 1 s |
| 5 000 | 60 | Pipeline al limite del CPU para Barnes-Hut |
| 20 000 | 30 50 | El cuello pasa a ser el layout (CPU); GPU render sigue holgado |
Si necesitas mas, fija los nodos (`pinned = true` o `Pause layout`) y veras 60 fps estables — el bottleneck es la simulacion, no el render.
## Anadir un demo nuevo
1. Anadir el prototipo en `demos.h` dentro de `namespace gallery`:
```cpp
void demo_my_thing();
```
2. Implementar el cuerpo en `demos_core.cpp` o `demos_viz.cpp` (o un fichero nuevo si la demo es grande, p.ej. `demos_graph.cpp`).
3. Registrar la entrada en el array `k_demos[]` de `main.cpp`:
```cpp
{"my_thing", "my_thing", "Core" /* o "Viz" */, &gallery::demo_my_thing},
```
4. Si la demo necesita `.cpp` adicionales del registry, anadirlos a `CMakeLists.txt` de la gallery.
5. Recompilar.
## Estructura
```
cpp/apps/primitives_gallery/
CMakeLists.txt # target primitives_gallery
README.md # este fichero
main.cpp # sidebar + router
demo.{h,cpp} # helpers (demo_header, section, code_block, ...)
demos.h # prototipos void demo_xxx()
demos_core.cpp # demos del dominio core
demos_viz.cpp # demos del dominio viz (charts simples)
demos_graph.cpp # demo de graph_viewport (mas pesada, fichero aparte)
```
## Convenciones para los demos
- **Sin estado real**: usar arrays sinteticos (`float fake[] = {...}`) o generadores deterministas con seed fijo. Datos reproducibles.
- **Sin red**: nunca llamar a `sqlite_api`, HTTP, filesystem. La gallery debe arrancar offline en cualquier maquina.
- **Snippets honestos**: el `code_block(...)` debe mostrar el codigo que produce esa demo, no pseudocodigo.
- **Variantes en grids**: si un primitivo tiene N variantes x M tamanos, mostrarlos todos en un `BeginTable` para comparacion lado-a-lado.
- **Estado static**: si la demo es interactiva (sliders, modal, etc.), guardar el estado en `static` locales — la gallery no destruye demos al cambiar de seccion, asi que el estado persiste hasta cerrar la app.
## Iconos en los demos
A partir de la sesion 2026-04-25 los demos usan los macros `TI_*` de `cpp/functions/core/icons_tabler.h` (Tabler v3.41.1, 5093 glyphs). La fuente la carga automaticamente `fn::run_app` via `icon_font_cpp_core`, y `add_imgui_app` copia `tabler-icons.ttf` junto al ejecutable post-build (no hay paso manual).
`demo_icon_button` y `demo_toolbar` (en `demos_core.cpp`) son la referencia visual: muestran el patron `button(TI_PLUS " New", V::Primary)` y la fila de iconos sueltos. Ver `cpp/DESIGN_SYSTEM.md` seccion 11 para la regla.
Si añades un demo nuevo y necesitas glyphs, **no metas `\x..` UTF-8 inline** — busca el icono en `icons_tabler.h` (o en https://tabler.io/icons) y usa el `TI_*` correspondiente.
-37
View File
@@ -1,37 +0,0 @@
---
name: primitives_gallery
lang: cpp
domain: gfx
description: "Visual catalog de primitivas C++ UI del fn_registry. Demos por categoria (charts, controls, layout, gl_info). Soporta modo --capture para regresion visual."
tags: [imgui, gallery, gfx, demo, capture]
uses_functions: []
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/primitives_gallery"
repo_url: ""
---
# primitives_gallery
Catalogo visual de las primitivas y componentes ImGui del registry. Cada demo se carga al hacer click en su entrada del sidebar.
## Build & run
```bash
cd cpp && cmake --build build --target primitives_gallery -j
./build/primitives_gallery
```
## Modo capture (regresion visual)
```bash
./build/primitives_gallery --capture <out_dir>
```
Renderiza cada demo offscreen y guarda PNGs en `<out_dir>/`. Permite gate visual via golden images.
## Notas
- `auto_dockspace = false` — usa `fullscreen_window` que ocupa todo el viewport.
- `init_gl_loader = true` — necesario para demos de OpenGL 4.3 core (compute, SSBOs).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 966 B

-173
View File
@@ -1,173 +0,0 @@
// Implementacion de gallery::run_capture — render offscreen + glReadPixels +
// PNG via stb_image_write. Ver capture.h.
#include "capture.h"
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include "implot.h"
#include "implot3d.h"
#include "core/tokens.h"
#include "core/icon_font.h"
#include "core/app_settings.h"
#include "gfx/gl_loader.h"
#include <GLFW/glfw3.h>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#include <cstdio>
#include <vector>
namespace gallery {
static void glfw_capture_error(int error, const char* description) {
std::fprintf(stderr, "GLFW Error %d: %s\n", error, description);
}
// Flip vertical in-place: OpenGL origin = bottom-left, PNG = top-left.
static void flip_vertical_rgba(unsigned char* px, int w, int h) {
const int stride = w * 4;
std::vector<unsigned char> row(stride);
for (int y = 0; y < h / 2; ++y) {
unsigned char* a = px + y * stride;
unsigned char* b = px + (h - 1 - y) * stride;
std::copy(a, a + stride, row.begin());
std::copy(b, b + stride, a);
std::copy(row.begin(), row.end(), b);
}
}
bool run_capture(const CaptureConfig& cfg, const std::vector<CaptureItem>& items) {
glfwSetErrorCallback(&glfw_capture_error);
if (!glfwInit()) {
std::fprintf(stderr, "capture: glfwInit failed\n");
return false;
}
// Capture mode usa GL 3.3 deliberadamente: WSL Mesa no entrega contexto
// 4.3 offscreen (GLXBadFBConfig). Las pruebas visuales no necesitan
// compute/SSBO — ImGui+ImPlot funciona en 3.3 core. La build interactiva
// (app_base.cpp) si pide 4.3.
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
GLFWwindow* window = glfwCreateWindow(
cfg.capture_w, cfg.capture_h, "capture", nullptr, nullptr);
if (!window) {
std::fprintf(stderr, "capture: glfwCreateWindow failed (no GL?)\n");
glfwTerminate();
return false;
}
glfwMakeContextCurrent(window);
glfwSwapInterval(0);
if (!fn::gfx::gl_loader_init()) {
std::fprintf(stderr, "capture: gl_loader_init failed\n");
glfwDestroyWindow(window);
glfwTerminate();
return false;
}
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImPlot::CreateContext();
ImPlot3D::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.IniFilename = nullptr; // no .ini side effects in capture mode.
io.DisplaySize = ImVec2((float)cfg.capture_w, (float)cfg.capture_h);
fn_ui::settings_load();
fn_ui::load_fonts_from_settings();
{
ImGuiStyle& style = ImGui::GetStyle();
style.FontSizeBase = fn_ui::settings().font_size_px;
style._NextFrameFontSizeBase = style.FontSizeBase;
}
fn_tokens::apply_dark_theme();
ImGui_ImplGlfw_InitForOpenGL(window, false);
ImGui_ImplOpenGL3_Init("#version 330");
bool ok_all = true;
std::vector<unsigned char> pixels((size_t)cfg.capture_w * cfg.capture_h * 4u);
for (const auto& item : items) {
// Warmup: rinde varios frames para que ImGui/ImPlot estabilicen layout
// (el primer frame frecuentemente carece de mediciones de tamaño).
for (int frame = 0; frame < cfg.warmup_frames + 1; ++frame) {
glfwPollEvents();
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// Ventana fullscreen sobre el viewport con la demo activa,
// sin sidebar (queremos el render del primitivo lo mas limpio
// posible para el diff visual).
const ImGuiViewport* vp = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(vp->WorkPos);
ImGui::SetNextWindowSize(vp->WorkSize);
ImGui::Begin("##capture_root",
nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoSavedSettings);
if (item.fn) item.fn();
ImGui::End();
ImGui::Render();
int dw, dh;
glfwGetFramebufferSize(window, &dw, &dh);
glViewport(0, 0, dw, dh);
glClearColor(fn_tokens::colors::bg.x,
fn_tokens::colors::bg.y,
fn_tokens::colors::bg.z, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
}
// Read framebuffer (GL_RGBA / GL_UNSIGNED_BYTE).
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, cfg.capture_w, cfg.capture_h,
GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
flip_vertical_rgba(pixels.data(), cfg.capture_w, cfg.capture_h);
char path[1024];
std::snprintf(path, sizeof(path), "%s/%s.png",
cfg.output_dir.c_str(), item.id.c_str());
const int rc = stbi_write_png(
path, cfg.capture_w, cfg.capture_h, 4,
pixels.data(), cfg.capture_w * 4);
if (rc == 0) {
std::fprintf(stderr, "capture: stbi_write_png failed for %s\n", path);
ok_all = false;
} else {
std::fprintf(stdout, "captured: %s\n", path);
}
}
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImPlot3D::DestroyContext();
ImPlot::DestroyContext();
ImGui::DestroyContext();
glfwDestroyWindow(window);
glfwTerminate();
return ok_all;
}
} // namespace gallery
-34
View File
@@ -1,34 +0,0 @@
#pragma once
// Capture mode: renderiza cada demo de la gallery en una ventana GLFW
// invisible y guarda un PNG en `output_dir/<demo_id>.png` via stb_image_write.
//
// Diseñado para CI / golden-image diffing: ver `cpp/scripts/update_goldens.sh`
// y `cpp/tests/test_visual.cpp`.
//
// Importante:
// - Requiere un contexto OpenGL real. En entornos sin GPU (containers minimos)
// funciona con `LIBGL_ALWAYS_SOFTWARE=1` (Mesa/llvmpipe) o swiftshader.
// - Si el entorno (WSL sin GL) no puede crear un contexto GL valido, el
// binario sale con codigo != 0 sin generar PNGs.
#include <string>
#include <vector>
namespace gallery {
struct CaptureItem {
std::string id;
void (*fn)();
};
struct CaptureConfig {
std::string output_dir;
int warmup_frames = 3;
int capture_w = 800;
int capture_h = 600;
};
// Devuelve true si todo el set se capturo OK.
bool run_capture(const CaptureConfig& cfg, const std::vector<CaptureItem>& items);
} // namespace gallery
-76
View File
@@ -1,76 +0,0 @@
#include "demo.h"
#include "core/tokens.h"
#include <cstdio>
namespace gallery {
void demo_header(const char* name, const char* version, const char* description) {
using namespace fn_tokens;
ImGui::SetWindowFontScale(1.4f);
ImGui::TextUnformatted(name);
ImGui::SetWindowFontScale(1.0f);
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::Text(" %s", version);
ImGui::PopStyleColor();
if (description && *description) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextWrapped("%s", description);
ImGui::PopStyleColor();
}
ImGui::Separator();
ImGui::Dummy(ImVec2(0, spacing::sm));
}
void section(const char* title) {
using namespace fn_tokens;
ImGui::Dummy(ImVec2(0, spacing::sm));
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextUnformatted(title);
ImGui::PopStyleColor();
ImGui::Separator();
ImGui::Dummy(ImVec2(0, spacing::xs));
}
void variant_label(const char* text) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim);
ImGui::TextUnformatted(text);
ImGui::PopStyleColor();
}
void code_block(const char* code) {
using namespace fn_tokens;
ImGui::Dummy(ImVec2(0, spacing::sm));
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::TextUnformatted("// example");
ImGui::PopStyleColor();
ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::bg);
ImGui::PushStyleColor(ImGuiCol_Border, colors::border);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, radius::sm);
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::md, spacing::sm));
// Altura: aprox lineas * line-height
int lines = 1;
for (const char* p = code; *p; ++p) if (*p == '\n') ++lines;
float h = lines * ImGui::GetTextLineHeightWithSpacing() + spacing::md;
char id[32];
std::snprintf(id, sizeof(id), "##code_%p", (const void*)code);
ImGui::BeginChild(id, ImVec2(0, h),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PushStyleColor(ImGuiCol_Text, colors::text);
ImGui::TextUnformatted(code);
ImGui::PopStyleColor();
ImGui::EndChild();
ImGui::PopStyleVar(3);
ImGui::PopStyleColor(2);
}
} // namespace gallery
-22
View File
@@ -1,22 +0,0 @@
#pragma once
// Helpers compartidos por todas las demos de la gallery.
// No son primitivos del registry — son utilidades locales de este app.
#include "imgui.h"
#include <string>
namespace gallery {
// Titulo + version + descripcion en la parte superior del panel derecho.
void demo_header(const char* name, const char* version, const char* description);
// Seccion secundaria dentro de una demo (agrupar variantes).
void section(const char* title);
// Bloque de codigo monoespaciado con bg surface y label "// example".
void code_block(const char* code);
// Etiqueta sutil encima de un grupo de widgets.
void variant_label(const char* text);
} // namespace gallery
-56
View File
@@ -1,56 +0,0 @@
#pragma once
// Cada demo_xxx() renderiza una seccion completa para un primitivo.
// Se llaman desde main.cpp en funcion del item seleccionado en el sidebar.
namespace gallery {
// --- Core ---
void demo_button();
void demo_icon_button();
void demo_toolbar();
void demo_modal();
void demo_text_input();
void demo_select();
void demo_toast();
void demo_tree_view();
void demo_kpi_card();
void demo_badge();
void demo_empty_state();
void demo_page_header();
void demo_dashboard_panel();
void demo_text_editor(); // wave 1, issue 0025
void demo_file_watcher(); // wave 1, issue 0025
void demo_process_runner();
void demo_tween(); // issue 0031
void demo_bezier_editor(); // issue 0031
void demo_timeline(); // issue 0031
void demo_sql_workbench(); // issue 0032
// --- Viz ---
void demo_bar_chart();
void demo_pie_chart();
void demo_line_plot();
void demo_scatter_plot();
void demo_histogram();
void demo_sparkline();
void demo_graph();
void demo_graph_styles(); // issue 0049f
void demo_candlestick();
void demo_gauge();
void demo_heatmap();
void demo_table_view();
void demo_surface_plot_3d(); // issue 0028, ImPlot3D
void demo_scatter_3d(); // issue 0028, ImPlot3D
void demo_mesh_viewer(); // issue 0029
void demo_treemap(); // issue 0034
void demo_sankey(); // issue 0034
void demo_chord(); // issue 0034
void demo_contour(); // issue 0034
void demo_voronoi(); // issue 0034
// --- Gfx ---
void demo_shader_canvas();
void demo_gl_texture(); // wave 1, issue 0026
void demo_gl_info(); // issue 0049b — runtime GL version + 4.3 caps
} // namespace gallery
-100
View File
@@ -1,100 +0,0 @@
// demos_3d — demos para los primitivos viz/* basados en ImPlot3D.
// Issue 0028: surface_plot_3d real + scatter_3d.
#include "demos.h"
#include "demo.h"
#include "viz/surface_plot_3d.h"
#include "viz/scatter_3d.h"
#include <imgui.h>
#include <cmath>
#include <random>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// surface_plot_3d
// ---------------------------------------------------------------------------
void demo_surface_plot_3d() {
demo_header("surface_plot_3d", "v2.0.0",
"Superficie 3D ImPlot3D (z = A * sin(fx*x) * cos(fy*y)) con sliders para "
"ajustar las frecuencias en tiempo real. Drag para orbitar, wheel para zoom.");
section("Malla 64x64 — sin(fx*x) * cos(fy*y)");
static float fx = 0.20f;
static float fy = 0.20f;
static float amp = 1.0f;
ImGui::SliderFloat("fx", &fx, 0.05f, 1.0f, "%.2f");
ImGui::SliderFloat("fy", &fy, 0.05f, 1.0f, "%.2f");
ImGui::SliderFloat("amplitud", &amp, 0.1f, 3.0f, "%.2f");
constexpr int N = 64;
static std::vector<float> z(N * N);
for (int j = 0; j < N; ++j) {
for (int i = 0; i < N; ++i) {
z[j * N + i] = amp * std::sin(fx * float(i)) * std::cos(fy * float(j));
}
}
fn::SurfacePlot3DConfig cfg{};
cfg.z = z.data();
cfg.nx = N; cfg.ny = N;
cfg.x_min = 0.f; cfg.x_max = float(N);
cfg.y_min = 0.f; cfg.y_max = float(N);
cfg.size = ImVec2(-1.f, 420.f);
fn::surface_plot_3d("##gallery_surface", cfg);
}
// ---------------------------------------------------------------------------
// scatter_3d
// ---------------------------------------------------------------------------
void demo_scatter_3d() {
demo_header("scatter_3d", "v1.0.0",
"Scatter 3D ImPlot3D con color por punto. 3 clusters gaussianos sinteticos "
"(N=500) para simular una visualizacion tipica de PCA / clustering.");
section("3 clusters gaussianos (500 puntos)");
constexpr int N = 500;
static std::vector<float> xs(N), ys(N), zs(N);
static std::vector<ImU32> colors(N);
static bool initialized = false;
if (!initialized) {
std::mt19937 rng(42);
std::normal_distribution<float> g(0.f, 0.4f);
const ImU32 palette[3] = {
IM_COL32(255, 99, 71, 255), // tomate
IM_COL32( 65, 170, 255, 255), // azul
IM_COL32(120, 220, 120, 255), // verde
};
const float cx[3] = {-1.5f, 1.5f, 0.f};
const float cy[3] = { 0.f, 0.f, 2.0f};
const float cz[3] = { 0.f, 1.0f,-1.0f};
for (int i = 0; i < N; ++i) {
int c = i % 3;
xs[i] = cx[c] + g(rng);
ys[i] = cy[c] + g(rng);
zs[i] = cz[c] + g(rng);
colors[i] = palette[c];
}
initialized = true;
}
fn::Scatter3DConfig cfg{};
cfg.xs = xs.data();
cfg.ys = ys.data();
cfg.zs = zs.data();
cfg.colors = colors.data();
cfg.n = N;
cfg.size = ImVec2(-1.f, 420.f);
fn::scatter_3d("##gallery_clusters", cfg);
}
} // namespace gallery
@@ -1,249 +0,0 @@
// Demos para los primitivos de animacion (issue 0031):
// - tween_curves
// - bezier_editor
// - timeline
#include "demos.h"
#include "demo.h"
#include "core/tween_curves.h"
#include "core/bezier_editor.h"
#include "core/timeline.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cstdio>
#include <cmath>
namespace gallery {
// ---------------------------------------------------------------------------
// demo_tween — dropdown + plot animado
// ---------------------------------------------------------------------------
void demo_tween() {
using namespace fn_tokens;
using fn::tween::Ease;
demo_header("tween_curves", "v1.0.0",
"Funciones de easing (Penner): linear, quad, cubic, expo, elastic, "
"bounce con variantes in/out/inOut. Header-mostly: el compilador "
"inlinea cada curva en el sitio de llamada.");
section("Selector + plot");
static int ease_idx = (int)Ease::OutCubic;
static float anim_t = 0.0f;
anim_t += ImGui::GetIO().DeltaTime * 0.5f;
if (anim_t > 1.5f) anim_t = -0.25f; // hold un poco antes de reiniciar
// Build labels
const char* labels[fn::tween::ease_count];
for (int i = 0; i < fn::tween::ease_count; i++) {
labels[i] = fn::tween::name((Ease)i);
}
ImGui::SetNextItemWidth(220.0f);
ImGui::Combo("##tween_ease", &ease_idx, labels, fn::tween::ease_count);
Ease ease = (Ease)ease_idx;
float t_clamped = anim_t;
if (t_clamped < 0.0f) t_clamped = 0.0f;
if (t_clamped > 1.0f) t_clamped = 1.0f;
float v = fn::tween::apply(ease, t_clamped);
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::Text(" t=%.2f f(t)=%.3f", t_clamped, v);
ImGui::PopStyleColor();
// Canvas plot
ImVec2 canvas_min = ImGui::GetCursorScreenPos();
ImVec2 canvas_size(360.0f, 220.0f);
ImVec2 canvas_max = ImVec2(canvas_min.x + canvas_size.x, canvas_min.y + canvas_size.y);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(canvas_min, canvas_max, ImGui::GetColorU32(colors::bg), radius::sm);
dl->AddRect(canvas_min, canvas_max, ImGui::GetColorU32(colors::border), radius::sm);
auto to_px = [&](float tx, float ty) {
// ty puede salir de [0,1] (elastic/bounce); damos algo de margen vertical.
return ImVec2(canvas_min.x + tx * canvas_size.x,
canvas_min.y + (1.0f - ty) * canvas_size.y);
};
// Grid 4x4
ImU32 grid = ImGui::GetColorU32(colors::border);
for (int i = 1; i < 4; i++) {
float fx = canvas_min.x + canvas_size.x * (float)i / 4.0f;
float fy = canvas_min.y + canvas_size.y * (float)i / 4.0f;
dl->AddLine(ImVec2(fx, canvas_min.y), ImVec2(fx, canvas_max.y), grid);
dl->AddLine(ImVec2(canvas_min.x, fy), ImVec2(canvas_max.x, fy), grid);
}
// Diagonal linear
dl->AddLine(to_px(0.0f, 0.0f), to_px(1.0f, 1.0f),
ImGui::GetColorU32(colors::text_dim), 1.0f);
// Curva
constexpr int N = 96;
ImVec2 prev = to_px(0.0f, fn::tween::apply(ease, 0.0f));
ImU32 col = ImGui::GetColorU32(colors::primary);
for (int i = 1; i <= N; i++) {
float x = (float)i / (float)N;
float y = fn::tween::apply(ease, x);
ImVec2 cur = to_px(x, y);
dl->AddLine(prev, cur, col, 2.0f);
prev = cur;
}
// Marker animado
ImVec2 m = to_px(t_clamped, v);
dl->AddCircleFilled(m, 5.0f, ImGui::GetColorU32(colors::primary_light));
dl->AddCircle(m, 6.0f, ImGui::GetColorU32(colors::text), 0, 1.5f);
// Avanzar cursor
ImGui::Dummy(canvas_size);
code_block(
"#include \"core/tween_curves.h\"\n\n"
"float k = fn::tween::apply(fn::tween::Ease::OutCubic, t);\n"
"// o named:\n"
"float k2 = fn::tween::out_cubic(t);"
);
}
// ---------------------------------------------------------------------------
// demo_bezier_editor — editor + plot evaluado
// ---------------------------------------------------------------------------
void demo_bezier_editor() {
using namespace fn_tokens;
demo_header("bezier_editor", "v1.0.0",
"Editor visual de curva Bezier cubica (4 puntos). Para diseñar "
"easing curves custom. p1/p2 son draggable; p0/p3 fijos en (0,0)/(1,1).");
section("Editor");
static fn::BezierCurve curve; // identidad por defecto: ease lineal con handles desplazados
if (ImGui::Button("Reset##bz_reset")) {
curve = fn::BezierCurve{};
}
ImGui::SameLine();
if (ImGui::Button("Ease-out preset##bz_eo")) {
curve = {{0,0}, {0.0f, 0.0f}, {0.58f, 1.0f}, {1,1}};
}
ImGui::SameLine();
if (ImGui::Button("Ease-in-out preset##bz_eio")) {
curve = {{0,0}, {0.42f, 0.0f}, {0.58f, 1.0f}, {1,1}};
}
fn::bezier_editor("##bz_editor", curve, ImVec2(220, 220));
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::Text("p0=(%.2f,%.2f) p1=(%.2f,%.2f) p2=(%.2f,%.2f) p3=(%.2f,%.2f)",
curve.p0.x, curve.p0.y, curve.p1.x, curve.p1.y,
curve.p2.x, curve.p2.y, curve.p3.x, curve.p3.y);
ImGui::PopStyleColor();
// Plot evaluation
section("bezier_eval(curve, t)");
static float t = 0.0f;
ImGui::SetNextItemWidth(360.0f);
ImGui::SliderFloat("t##bz_t", &t, 0.0f, 1.0f, "%.3f");
float y = fn::bezier_eval(curve, t);
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::Text("y(t=%.3f) = %.3f", t, y);
ImGui::PopStyleColor();
code_block(
"#include \"core/bezier_editor.h\"\n\n"
"static fn::BezierCurve curve;\n"
"if (fn::bezier_editor(\"##my\", curve, ImVec2(220, 220))) {\n"
" // user dragged a control point\n"
"}\n"
"float k = fn::bezier_eval(curve, t);"
);
}
// ---------------------------------------------------------------------------
// demo_timeline — 2 tracks + display
// ---------------------------------------------------------------------------
void demo_timeline() {
using namespace fn_tokens;
using fn::tween::Ease;
demo_header("timeline", "v1.0.0",
"Timeline tipo DAW: tracks horizontales con keyframes draggable, "
"scrub con el ruler, play/pause/loop. track_value_at(t) interpola "
"aplicando la Ease de cada keyframe destino.");
static fn::TimelineState tl;
static bool inited = false;
if (!inited) {
tl.duration = 4.0f;
tl.playing = true;
tl.tracks.push_back({"hue", {
{0.0f, 0.0f, Ease::Linear},
{2.0f, 1.0f, Ease::OutCubic},
{4.0f, 0.0f, Ease::InOutCubic},
}});
tl.tracks.push_back({"amp", {
{0.0f, 0.2f, Ease::Linear},
{3.0f, 1.0f, Ease::OutElastic},
}});
inited = true;
}
// Update
fn::timeline_update(tl, ImGui::GetIO().DeltaTime);
// Display values
section("Live values");
float hue = fn::track_value_at(tl.tracks[0], tl.current_time);
float amp = fn::track_value_at(tl.tracks[1], tl.current_time);
ImGui::PushStyleColor(ImGuiCol_Text, colors::text);
ImGui::Text("t = %.3fs", tl.current_time);
ImGui::PopStyleColor();
auto draw_bar = [&](const char* name, float value, float vmin, float vmax) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::Text("%-4s", name);
ImGui::PopStyleColor();
ImGui::SameLine();
ImVec2 cmin = ImGui::GetCursorScreenPos();
ImVec2 csize = ImVec2(280.0f, 14.0f);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(cmin, ImVec2(cmin.x + csize.x, cmin.y + csize.y),
ImGui::GetColorU32(colors::surface_active), radius::sm);
float k = (value - vmin) / (vmax - vmin);
if (k < 0.0f) k = 0.0f;
if (k > 1.0f) k = 1.0f;
dl->AddRectFilled(cmin, ImVec2(cmin.x + csize.x * k, cmin.y + csize.y),
ImGui::GetColorU32(colors::primary), radius::sm);
ImGui::Dummy(csize);
ImGui::SameLine();
ImGui::Text("%.3f", value);
};
draw_bar("hue", hue, 0.0f, 1.0f);
draw_bar("amp", amp, 0.0f, 1.0f);
section("Widget");
fn::timeline_widget("##gallery_tl", tl, ImVec2(-1, 220));
code_block(
"#include \"core/timeline.h\"\n\n"
"static fn::TimelineState tl;\n"
"tl.tracks.push_back({\"hue\", {{0,0}, {2,1, fn::tween::Ease::OutCubic}, {4,0}}});\n"
"tl.duration = 4.0f; tl.playing = true;\n\n"
"fn::timeline_update(tl, ImGui::GetIO().DeltaTime);\n"
"float h = fn::track_value_at(tl.tracks[0], tl.current_time);\n"
"fn::timeline_widget(\"##tl\", tl);"
);
}
} // namespace gallery
-447
View File
@@ -1,447 +0,0 @@
#include "demos.h"
#include "demo.h"
#include "core/button.h"
#include "core/icon_button.h"
#include "core/toolbar.h"
#include "core/modal_dialog.h"
#include "core/text_input.h"
#include "core/select.h"
#include "core/toast.h"
#include "core/tree_view.h"
#include "core/badge.h"
#include "core/empty_state.h"
#include "core/page_header.h"
#include "core/dashboard_panel.h"
#include "core/tokens.h"
#include "core/icons_tabler.h"
#include "viz/kpi_card.h"
#include <imgui.h>
#include <cstdio>
using namespace fn_ui;
using V = ButtonVariant;
using S = ButtonSize;
namespace gallery {
// ---------------------------------------------------------------------------
// button
// ---------------------------------------------------------------------------
void demo_button() {
demo_header("button", "v1.0.0",
"Boton con 4 variantes semanticas y 3 tamanos. Usa tokens para colores, "
"radius y padding — estilo consistente en toda la app.");
section("Variants x Sizes");
const V variants[] = {V::Primary, V::Secondary, V::Subtle, V::Danger};
const char* variant_names[] = {"Primary", "Secondary", "Subtle", "Danger"};
const S sizes[] = {S::Sm, S::Md, S::Lg};
const char* size_names[] = {"sm", "md", "lg"};
if (ImGui::BeginTable("##btn_grid", 5, ImGuiTableFlags_SizingFixedFit)) {
ImGui::TableSetupColumn("size");
for (int c = 0; c < 4; c++) ImGui::TableSetupColumn(variant_names[c]);
ImGui::TableHeadersRow();
for (int s = 0; s < 3; s++) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
variant_label(size_names[s]);
for (int v = 0; v < 4; v++) {
ImGui::TableSetColumnIndex(v + 1);
char id[32];
std::snprintf(id, sizeof(id), "%s##%d%d", variant_names[v], s, v);
button(id, variants[v], sizes[s]);
}
}
ImGui::EndTable();
}
code_block(
"#include \"core/button.h\"\n"
"using fn_ui::button;\n"
"using V = fn_ui::ButtonVariant;\n\n"
"if (button(\"Save\", V::Primary)) save();\n"
"if (button(\"Cancel\", V::Subtle)) close();\n"
"if (button(\"Delete\", V::Danger)) confirm();"
);
}
// ---------------------------------------------------------------------------
// icon_button
// ---------------------------------------------------------------------------
void demo_icon_button() {
demo_header("icon_button", "v1.0.0",
"Boton cuadrado 28x28 con un glyph centrado y tooltip opcional. "
"Usa los TI_* de core/icons_tabler.h (Tabler Icons cargado automaticamente "
"por fn::run_app via icon_font.cpp).");
section("Tabler icon set");
struct { const char* id; const char* glyph; const char* tip; } ic[] = {
{"##rl", TI_REFRESH, "Reload"},
{"##ad", TI_PLUS, "Add"},
{"##dl", TI_TRASH, "Delete"},
{"##dn", TI_CHEVRON_DOWN, "Dropdown"},
{"##cf", TI_SETTINGS, "Settings"},
{"##ok", TI_CHECK, "Check"},
{"##cl", TI_X, "Close"},
{"##ed", TI_PENCIL, "Edit"},
{"##sv", TI_DEVICE_FLOPPY, "Save"},
{"##sr", TI_SEARCH, "Search"},
{"##hp", TI_HELP, "Help"},
{"##hm", TI_HOME, "Home"},
};
for (auto& b : ic) {
icon_button(b.id, b.glyph, b.tip);
ImGui::SameLine();
}
ImGui::NewLine();
code_block(
"#include \"core/icons_tabler.h\"\n\n"
"if (icon_button(\"##reload\", TI_REFRESH, \"Reload\"))\n"
" reload_data();\n\n"
"// Mas de 5000 iconos disponibles — ver core/icons_tabler.h"
);
}
// ---------------------------------------------------------------------------
// toolbar
// ---------------------------------------------------------------------------
void demo_toolbar() {
demo_header("toolbar", "v1.0.0",
"Grupo horizontal con spacing consistente y separadores verticales sutiles. "
"El caller usa ImGui::SameLine entre items y toolbar_separator entre grupos.");
section("Example with two groups");
toolbar_begin();
button(TI_PLUS " New", V::Primary); ImGui::SameLine();
button(TI_FOLDER_OPEN " Open", V::Secondary); ImGui::SameLine();
button(TI_DEVICE_FLOPPY " Save",V::Secondary);
toolbar_separator();
icon_button("##set", TI_SETTINGS, "Settings");
ImGui::SameLine();
icon_button("##help", TI_HELP, "Help");
toolbar_end();
code_block(
"#include \"core/icons_tabler.h\"\n\n"
"toolbar_begin();\n"
" button(TI_PLUS \" New\", V::Primary); ImGui::SameLine();\n"
" button(TI_FOLDER_OPEN \" Open\", V::Secondary);\n"
" toolbar_separator();\n"
" icon_button(\"##set\", TI_SETTINGS, \"Settings\");\n"
"toolbar_end();"
);
}
// ---------------------------------------------------------------------------
// modal_dialog
// ---------------------------------------------------------------------------
void demo_modal() {
demo_header("modal_dialog", "v1.0.0",
"Popup modal centrada con estilo surface+border. Close con Escape o click en X. "
"Patron begin/end — modal_dialog_end debe llamarse siempre.");
static bool show = false;
if (button("Open modal", V::Primary)) show = true;
if (modal_dialog_begin("Demo modal", &show, ImVec2(380, 0))) {
ImGui::TextWrapped(
"Modal centrada en el viewport principal, con estilo tokens.");
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
static char buf[64] = {};
text_input("Name", buf, sizeof(buf), "escribe algo");
ImGui::Separator();
if (button("Cancel", V::Subtle)) show = false;
ImGui::SameLine();
if (button("Done", V::Primary)) show = false;
}
modal_dialog_end();
code_block(
"static bool show = false;\n"
"if (button(\"Open\", Primary)) show = true;\n"
"if (modal_dialog_begin(\"Title\", &show, ImVec2(380,0))) {\n"
" // ... campos del form ...\n"
" if (button(\"Done\", Primary)) show = false;\n"
"}\n"
"modal_dialog_end();"
);
}
// ---------------------------------------------------------------------------
// text_input
// ---------------------------------------------------------------------------
void demo_text_input() {
demo_header("text_input", "v1.0.0",
"Label muted + input estilizado con tokens. Full-width dentro del contenedor. "
"Placeholder opcional mostrado en text_dim cuando el buffer esta vacio.");
static char name[128] = {};
static char desc[256] = {};
static char tags[128] = {};
ImGui::BeginChild("##ti_wrap", ImVec2(420, 0), ImGuiChildFlags_AutoResizeY);
text_input("Name", name, sizeof(name), "my-new-thing");
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
text_input("Description", desc, sizeof(desc));
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
text_input("Tags (CSV)", tags, sizeof(tags), "imgui,ui,form");
ImGui::EndChild();
code_block(
"static char name[128] = {};\n"
"text_input(\"Name\", name, sizeof(name), \"my-new-thing\");\n"
"// true on change — se usa mas para validar en vivo\n"
"// que para leer el valor (que vive en el buffer)."
);
}
// ---------------------------------------------------------------------------
// select
// ---------------------------------------------------------------------------
void demo_select() {
demo_header("select", "v1.0.0",
"Dropdown con label muted y opcion (none) opcional. Mismo estilo tokens que text_input.");
static int lang_idx = 0;
static int domain_idx = -1;
const char* langs[] = {"go", "py", "ts", "sh", "cpp"};
const char* domains[] = {"core", "infra", "finance", "datascience", "viz"};
ImGui::BeginChild("##sl_wrap", ImVec2(420, 0), ImGuiChildFlags_AutoResizeY);
select("Language", &lang_idx, langs, 5);
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
select("Domain (optional)", &domain_idx, domains, 5, true);
ImGui::EndChild();
code_block(
"static int lang = 0;\n"
"const char* langs[] = {\"go\",\"py\",\"ts\",\"sh\",\"cpp\"};\n"
"select(\"Language\", &lang, langs, 5);"
);
}
// ---------------------------------------------------------------------------
// toast + inbox
// ---------------------------------------------------------------------------
void demo_toast() {
demo_header("toast", "v1.1.0",
"Notificaciones efimeras (~3.5s con fade-out) + inbox con campana. "
"La campana muestra badge con no-leidos y popover con las ultimas 50.");
section("Trigger toasts");
if (button("Info", V::Secondary)) toast_push(ToastKind::Info, "this is an info toast");
ImGui::SameLine();
if (button("Success", V::Primary)) toast_push(ToastKind::Success, "operation completed");
ImGui::SameLine();
if (button("Warning", V::Secondary)) toast_push(ToastKind::Warning, "heads up about something");
ImGui::SameLine();
if (button("Error", V::Danger)) toast_push(ToastKind::Error, "operation failed: reason");
section("Inbox (bell with unread badge)");
toast_inbox_button("##inbox_demo");
code_block(
"toast_push(ToastKind::Success, \"Reindexed 891 functions\");\n"
"toast_push(ToastKind::Error, \"HTTP 503: server down\");\n\n"
"// En la toolbar:\n"
"toast_inbox_button(\"##inbox\");\n\n"
"// Una vez por frame al final del render:\n"
"toast_render();"
);
}
// ---------------------------------------------------------------------------
// tree_view
// ---------------------------------------------------------------------------
void demo_tree_view() {
demo_header("tree_view", "v1.0.0",
"Tree low-level para jerarquias (ej. projects -> apps/analysis/vaults). "
"Sin estado interno: el caller gestiona seleccion y pasa 'selected' por parametro.");
static std::string selected;
section("Projects (fake)");
ImGui::BeginChild("##tv", ImVec2(360, 200), ImGuiChildFlags_Borders);
struct FakeProject { const char* id; const char* name; const char* apps[3]; };
const FakeProject projs[] = {
{"app_turismo", "app_turismo", {"guide_es", "offline_maps", nullptr}},
{"element_agents", "element_agents", {"matrix_bot", nullptr, nullptr}},
{"fn_monitoring", "fn_monitoring", {"sqlite_api", "registry_dashboard", nullptr}},
};
for (auto& p : projs) {
bool sel = (selected == p.id);
if (tree_branch_begin(p.id, p.name, sel)) {
if (tree_node_clicked()) selected = p.id;
for (int i = 0; i < 3 && p.apps[i]; i++) {
bool asel = (selected == p.apps[i]);
tree_leaf(p.apps[i], p.apps[i], asel);
if (tree_node_clicked()) selected = p.apps[i];
}
tree_branch_end();
} else if (tree_node_clicked()) {
selected = p.id;
}
}
ImGui::EndChild();
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::Text("Selected: %s", selected.empty() ? "(none)" : selected.c_str());
ImGui::PopStyleColor();
code_block(
"static std::string sel;\n"
"if (tree_branch_begin(p.id, p.name, sel == p.id)) {\n"
" if (tree_node_clicked()) sel = p.id;\n"
" for (auto& a : p.apps) {\n"
" tree_leaf(a.id, a.name, sel == a.id);\n"
" if (tree_node_clicked()) sel = a.id;\n"
" }\n"
" tree_branch_end();\n"
"}"
);
}
// ---------------------------------------------------------------------------
// kpi_card
// ---------------------------------------------------------------------------
void demo_kpi_card() {
demo_header("kpi_card", "v1.3.0",
"Card compacta 86px con icono opcional + label muted, valor x1.4, trend con "
"TI_TRENDING_UP/DOWN y sparkline. Usa tokens: surface bg, border, radius md.");
if (ImGui::BeginTable("##kpi_grid", 4, ImGuiTableFlags_SizingStretchSame)) {
float history[] = {10, 12, 11, 15, 18, 17, 20};
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f", TI_CASH);
ImGui::TableSetColumnIndex(1); kpi_card("Users", 1250.0f, 3.4f, history, 7, "%.0f", TI_USERS);
ImGui::TableSetColumnIndex(2); kpi_card("Churn", 2.1f, -0.3f, history, 7, "%.1f%%", TI_CHART_BAR);
ImGui::TableSetColumnIndex(3); kpi_card("Errors", 0.0f, 0.0f, nullptr, 0, "%.0f", TI_ALERT_CIRCLE);
ImGui::EndTable();
}
code_block(
"#include \"core/icons_tabler.h\"\n\n"
"float history[] = {10,12,11,15,18,17,20};\n"
"kpi_card(\"Revenue\", 20000.0f, 12.5f, history, 7, \"$%.0f\", TI_CASH);\n"
"kpi_card(\"Users\", 1250.0f, 3.4f, history, 7, \"%.0f\", TI_USERS);\n"
"// Sin delta ni history: muestra TI_MINUS como placeholder\n"
"kpi_card(\"Errors\", 0.0f, 0.0f, nullptr, 0, \"%.0f\", TI_ALERT_CIRCLE);"
);
}
// ---------------------------------------------------------------------------
// badge
// ---------------------------------------------------------------------------
void demo_badge() {
demo_header("badge", "v1.0.0",
"Etiqueta inline con 6 variantes semanticas. Equivalente a <Badge> de fn_library.");
section("Variants");
badge("Default", BadgeVariant::Default); ImGui::SameLine();
badge("Success", BadgeVariant::Success); ImGui::SameLine();
badge("Warning", BadgeVariant::Warning); ImGui::SameLine();
badge("Error", BadgeVariant::Error); ImGui::SameLine();
badge("Info", BadgeVariant::Info); ImGui::SameLine();
badge("Outline", BadgeVariant::Outline);
section("In context (table row)");
ImGui::Text("filter_slice_go_core"); ImGui::SameLine();
badge("pure", BadgeVariant::Success); ImGui::SameLine();
badge("tested", BadgeVariant::Info);
code_block(
"badge(\"pure\", BadgeVariant::Success);\n"
"badge(\"stale\", BadgeVariant::Warning);\n"
"badge(\"broken\", BadgeVariant::Error);"
);
}
// ---------------------------------------------------------------------------
// empty_state
// ---------------------------------------------------------------------------
void demo_empty_state() {
demo_header("empty_state", "v1.0.0",
"Icono grande muted + titulo + descripcion opcional. Para listas/tablas vacias.");
ImGui::BeginChild("##es", ImVec2(0, 180), ImGuiChildFlags_Borders);
empty_state("( no data )", "No projects yet",
"Create one under projects/{name}/ with project.md and reindex");
ImGui::EndChild();
code_block(
"if (apps.empty()) {\n"
" empty_state(\"( no data )\", \"No apps yet\",\n"
" \"Click + Add to create one\");\n"
" return;\n"
"}"
);
}
// ---------------------------------------------------------------------------
// page_header
// ---------------------------------------------------------------------------
void demo_page_header() {
demo_header("page_header", "v1.0.0",
"Header de pagina con titulo, subtitulo opcional y separador final. "
"Patron begin/end permite insertar acciones entre titulo y separador.");
page_header_begin("Dashboard", "13 apps, 3 projects, 2 analyses");
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 140.0f);
toolbar_begin();
button("Reload", V::Subtle); ImGui::SameLine();
button("+ Add", V::Secondary);
toolbar_end();
page_header_end();
code_block(
"page_header_begin(\"Dashboard\", subtitle);\n"
"ImGui::SameLine(ImGui::GetContentRegionAvail().x - 140);\n"
"toolbar_begin();\n"
" button(\"Reload\", Subtle);\n"
"toolbar_end();\n"
"page_header_end();"
);
}
// ---------------------------------------------------------------------------
// dashboard_panel
// ---------------------------------------------------------------------------
void demo_dashboard_panel() {
demo_header("dashboard_panel", "v1.0.0",
"Contenedor tipo panel con titulo, bordes redondeados, bg surface. "
"Auto-resize-Y segun contenido. Usa min_width/min_height como piso.");
if (dashboard_panel_begin("Revenue", 0, 120.0f)) {
ImGui::Text("Some panel content goes here.");
ImGui::Text("Anything drawn inside lives in the child window.");
}
dashboard_panel_end();
code_block(
"if (dashboard_panel_begin(\"Revenue\", 0, 120.0f)) {\n"
" ImGui::Text(\"content\");\n"
"}\n"
"dashboard_panel_end();"
);
}
} // namespace gallery
@@ -1,215 +0,0 @@
// Demos faltantes: process_runner (Core), candlestick / gauge / heatmap /
// table_view (Viz). Aniade cobertura sobre los primitivos del registry que
// no tenian su entry en la gallery.
#include "demos.h"
#include "demo.h"
#include "core/process_runner.h"
#include "viz/candlestick.h"
#include "viz/gauge.h"
#include "viz/heatmap.h"
#include "viz/table_view.h"
#include <imgui.h>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <thread>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// process_runner (Core)
// ---------------------------------------------------------------------------
void demo_process_runner() {
demo_header("process_runner", "v1.0.0",
"Ejecuta una tarea en std::thread en background y expone estado thread-safe "
"(idle/running/success/error). El widget runner_status() dibuja inline un "
"spinner mientras corre y un mensaje de Success/Error al terminar.");
static fn_ui::ProcessRunner runner;
section("Tarea simulada (sleep 2s)");
{
if (ImGui::Button("Run task")) {
if (!runner.is_busy()) {
fn_ui::runner_trigger(runner, [](std::string& out) -> bool {
std::this_thread::sleep_for(std::chrono::seconds(2));
out = "task done in 2s";
return true;
});
}
}
ImGui::SameLine();
if (ImGui::Button("Run failing task")) {
if (!runner.is_busy()) {
fn_ui::runner_trigger(runner, [](std::string& out) -> bool {
std::this_thread::sleep_for(std::chrono::seconds(1));
out = "simulated failure";
return false;
});
}
}
ImGui::SameLine();
if (ImGui::Button("Reset")) runner.reset();
fn_ui::runner_status(runner, "Working...");
}
code_block(
"static fn_ui::ProcessRunner r;\n"
"if (button(\"Run\", Primary) && !r.is_busy()) {\n"
" fn_ui::runner_trigger(r, [](std::string& out) -> bool {\n"
" return do_work(&out);\n"
" });\n"
"}\n"
"fn_ui::runner_status(r, \"Working...\");"
);
}
// ---------------------------------------------------------------------------
// candlestick (Viz)
// ---------------------------------------------------------------------------
void demo_candlestick() {
demo_header("candlestick", "v1.0.0",
"Grafico de velas OHLC con ImPlot custom rendering. Verde si close >= open, "
"rojo si bajista. Tooltip al hover muestra OHLC del dia.");
section("OHLC sintetico (30 dias)");
{
static std::vector<double> dates, opens, closes, lows, highs;
if (dates.empty()) {
dates.reserve(30); opens.reserve(30); closes.reserve(30);
lows.reserve(30); highs.reserve(30);
double price = 100.0;
for (int i = 0; i < 30; ++i) {
double drift = std::sin(i * 0.4) * 1.2;
double o = price;
double c = price + drift + (i % 3 == 0 ? -0.6 : 0.4);
double l = std::min(o, c) - 0.8 - (i % 5) * 0.1;
double h = std::max(o, c) + 0.8 + (i % 4) * 0.1;
dates.push_back(double(i));
opens.push_back(o);
closes.push_back(c);
lows.push_back(l);
highs.push_back(h);
price = c;
}
}
candlestick("##cs", dates.data(), opens.data(), closes.data(),
lows.data(), highs.data(), int(dates.size()));
}
code_block(
"candlestick(\"##cs\", dates, opens, closes, lows, highs, n,\n"
" /*width_percent=*/0.25f, /*tooltip=*/true);"
);
}
// ---------------------------------------------------------------------------
// gauge (Viz)
// ---------------------------------------------------------------------------
void demo_gauge() {
demo_header("gauge", "v1.0.0",
"Indicador circular tipo velocimetro con ImGui DrawList. Color interpolado "
"verde -> amarillo -> rojo segun el valor normalizado.");
static float v_cpu = 0.32f, v_mem = 0.78f, v_gpu = 0.55f;
section("Tres gauges con sliders");
{
ImGui::SliderFloat("cpu", &v_cpu, 0.0f, 1.0f);
ImGui::SliderFloat("mem", &v_mem, 0.0f, 1.0f);
ImGui::SliderFloat("gpu", &v_gpu, 0.0f, 1.0f);
ImGui::Spacing();
ImGui::BeginGroup();
gauge("CPU", v_cpu, 0.0f, 1.0f, 60.0f);
ImGui::EndGroup();
ImGui::SameLine(0.0f, 24.0f);
ImGui::BeginGroup();
gauge("MEM", v_mem, 0.0f, 1.0f, 60.0f);
ImGui::EndGroup();
ImGui::SameLine(0.0f, 24.0f);
ImGui::BeginGroup();
gauge("GPU", v_gpu, 0.0f, 1.0f, 60.0f);
ImGui::EndGroup();
}
code_block("gauge(\"CPU\", 0.32f, 0.0f, 1.0f, 60.0f);");
}
// ---------------------------------------------------------------------------
// heatmap (Viz)
// ---------------------------------------------------------------------------
void demo_heatmap() {
demo_header("heatmap", "v1.0.0",
"Mapa de calor 2D con ImPlot. Datos row-major. Util para correlation "
"matrices, attention maps, distribuciones 2D discretas.");
constexpr int R = 12;
constexpr int C = 12;
static float values[R * C] = {0};
static bool init = false;
if (!init) {
for (int r = 0; r < R; ++r) {
for (int c = 0; c < C; ++c) {
float dx = (c - C * 0.5f) / float(C);
float dy = (r - R * 0.5f) / float(R);
values[r * C + c] = std::exp(-(dx * dx + dy * dy) * 6.0f);
}
}
init = true;
}
section("Gaussian 12x12");
{
heatmap("##hm", values, R, C, 0.0f, 1.0f);
}
code_block(
"float values[R * C];\n"
"// fill row-major: values[r * C + c] = ...\n"
"heatmap(\"##hm\", values, R, C, /*min=*/0.0f, /*max=*/1.0f);"
);
}
// ---------------------------------------------------------------------------
// table_view (Viz)
// ---------------------------------------------------------------------------
void demo_table_view() {
demo_header("table_view", "v1.0.0",
"Tabla interactiva con sorting indicators y scroll usando la ImGui Tables API. "
"Headers + cells row-major. Util para dashboards y inspectores.");
section("Lenguajes del registry");
{
const char* headers[] = {"id", "lang", "domain", "purity"};
// 6 filas x 4 cols, row-major
const char* cells[] = {
"filter_slice_go_core", "go", "core", "pure",
"metabase_setup_py_infra", "py", "infra", "impure",
"rsync_deploy_bash_infra", "sh", "infra", "impure",
"button_cpp_core", "cpp", "core", "pure",
"gl_texture_load_cpp_gfx", "cpp", "gfx", "impure",
"audio_fft_cpp_core", "cpp", "core", "pure",
};
const int row_count = 6;
const int col_count = 4;
table_view("##tbl", headers, col_count, cells, row_count);
}
code_block(
"const char* headers[] = {\"id\", \"lang\", \"domain\"};\n"
"const char* cells[] = {/* row-major: r0c0,r0c1,r0c2, r1c0,... */};\n"
"table_view(\"##tbl\", headers, 3, cells, n_rows);"
);
}
} // namespace gallery
-196
View File
@@ -1,196 +0,0 @@
// Demos del dominio gfx — primitivos OpenGL/shader que viven en
// cpp/functions/gfx/. La pieza distintiva de shaders_lab es el
// shader_canvas: framebuffer + fullscreen quad + programa GL animado por
// time/resolution/mouse.
#include "demos.h"
#include "demo.h"
#include "gfx/shader_canvas.h"
#include "gfx/gl_shader.h"
#include "gfx/gl_loader.h"
#include <imgui.h>
#include <chrono>
namespace gallery {
namespace {
// Fragment shader sintetico — gradiente animado con celdas. Usa los uniforms
// estandar que compile_fragment inyecta: u_resolution, u_time, u_mouse.
const char* kShaderSrc = R"(
void mainImage() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec2 cell = uv * 8.0;
vec2 ipos = floor(cell);
vec2 fpos = fract(cell) - 0.5;
float t = u_time * 0.6;
float wave = sin(ipos.x * 0.7 + ipos.y * 0.5 + t);
float dist = length(fpos);
vec3 a = vec3(0.30, 0.43, 0.96); // indigo
vec3 b = vec3(0.95, 0.45, 0.85); // pink
vec3 col = mix(a, b, 0.5 + 0.5 * wave);
// Mouse focus: oscurecemos celdas lejanas al cursor.
vec2 m = u_mouse / u_resolution;
float fm = 1.0 - smoothstep(0.0, 0.6, length(uv - m));
col *= 0.6 + 0.4 * fm;
// Disco interior por celda con borde suave.
col *= smoothstep(0.5, 0.45, dist);
fragColor = vec4(col, 1.0);
}
void main() {
mainImage();
}
)";
struct CanvasState {
fn::gfx::ShaderCanvas canvas;
bool compiled = false;
bool compile_failed = false;
std::string err_msg;
std::chrono::steady_clock::time_point t0;
};
CanvasState& state() {
static CanvasState s;
return s;
}
} // namespace
void demo_shader_canvas() {
demo_header("shader_canvas", "v1.0.0",
"Framebuffer + fullscreen quad + shader GLSL animado. La misma pieza "
"que usa shaders_lab para el preview en vivo. Uniforms u_time / u_resolution / u_mouse "
"los inyecta gl_shader::compile_fragment automaticamente.");
auto& s = state();
// Compilacion lazy (en el primer frame ya hay contexto GL valido).
if (!s.compiled && !s.compile_failed) {
fn::gfx::gl_loader_init();
fn::gfx::canvas_init(s.canvas);
auto cr = fn::gfx::compile_fragment(kShaderSrc);
if (!cr.ok) {
s.compile_failed = true;
s.err_msg = cr.err_msg;
} else {
fn::gfx::canvas_set_program(s.canvas, cr.program);
s.t0 = std::chrono::steady_clock::now();
s.compiled = true;
}
}
if (s.compile_failed) {
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1),
"Compilacion del fragment shader fallo:\n%s",
s.err_msg.c_str());
return;
}
section("Live preview");
// Render del shader en un panel ~480x300 px. canvas_render hace resize
// automatico segun GetContentRegionAvail si lo dejas crecer.
ImGui::BeginChild("##shader_preview", ImVec2(480, 300),
ImGuiChildFlags_Borders);
const float dt = std::chrono::duration<float>(
std::chrono::steady_clock::now() - s.t0).count();
fn::gfx::canvas_render(s.canvas, dt);
ImGui::EndChild();
code_block(
"#include \"gfx/shader_canvas.h\"\n"
"#include \"gfx/gl_shader.h\"\n\n"
"static fn::gfx::ShaderCanvas canvas;\n"
"// Setup (una vez):\n"
"fn::gfx::canvas_init(canvas);\n"
"auto cr = fn::gfx::compile_fragment(user_glsl);\n"
"if (cr.ok) fn::gfx::canvas_set_program(canvas, cr.program);\n\n"
"// Cada frame, dentro de un Begin/End:\n"
"fn::gfx::canvas_render(canvas, time_seconds);"
);
}
// Issue 0049b — Mostrar la version de OpenGL del contexto y un puñado de
// limites 4.3 que confirman que compute shaders / SSBO / image load-store
// estan disponibles. No es codigo del registry, solo introspeccion del
// driver — sin estado, sin side effects: solo glGetString + glGetIntegerv.
#ifndef GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS
#define GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS 0x90DD
#endif
#ifndef GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS
#define GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS 0x90DC
#endif
#ifndef GL_MAX_COMPUTE_SHARED_MEMORY_SIZE
#define GL_MAX_COMPUTE_SHARED_MEMORY_SIZE 0x8262
#endif
void demo_gl_info() {
demo_header("gl_info", "v1.0.0",
"Introspeccion del contexto OpenGL activo (issue 0049b). El framework "
"ahora pide GL 4.3 core, lo que habilita compute shaders, SSBOs, image "
"load/store y atomic counters — bloques esenciales del graph_renderer "
"GPU del proyecto osint_graph.");
auto gl_str = [](GLenum e) -> const char* {
const GLubyte* s = glGetString(e);
return s ? reinterpret_cast<const char*>(s) : "(null)";
};
section("Driver");
ImGui::Text("Vendor: %s", gl_str(GL_VENDOR));
ImGui::Text("Renderer: %s", gl_str(GL_RENDERER));
ImGui::Text("Version: %s", gl_str(GL_VERSION));
ImGui::Text("GLSL: %s", gl_str(GL_SHADING_LANGUAGE_VERSION));
GLint major = 0, minor = 0;
glGetIntegerv(GL_MAJOR_VERSION, &major);
glGetIntegerv(GL_MINOR_VERSION, &minor);
const bool has_43 = (major > 4) || (major == 4 && minor >= 3);
section("Capabilities");
ImGui::Text("Context: %d.%d core", major, minor);
if (has_43) {
ImGui::TextColored(ImVec4(0.40f, 0.85f, 0.40f, 1.0f),
"OpenGL 4.3+ — compute shaders, SSBOs, image load/store, atomic counters: AVAILABLE");
} else {
ImGui::TextColored(ImVec4(0.95f, 0.55f, 0.30f, 1.0f),
"OpenGL < 4.3 — compute shaders / SSBOs NOT available on this driver");
}
section("Limits");
GLint v = 0;
auto row = [&](const char* label, GLenum e) {
v = 0;
glGetIntegerv(e, &v);
ImGui::Text("%-44s %d", label, v);
};
row("GL_MAX_TEXTURE_SIZE", GL_MAX_TEXTURE_SIZE);
row("GL_MAX_VERTEX_ATTRIBS", GL_MAX_VERTEX_ATTRIBS);
row("GL_MAX_UNIFORM_BLOCK_SIZE", GL_MAX_UNIFORM_BLOCK_SIZE);
if (has_43) {
row("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS);
row("GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS);
row("GL_MAX_COMPUTE_SHARED_MEMORY_SIZE", GL_MAX_COMPUTE_SHARED_MEMORY_SIZE);
}
code_block(
"// Solo glGetString + glGetIntegerv — sin loader extra.\n"
"GLint major = 0, minor = 0;\n"
"glGetIntegerv(GL_MAJOR_VERSION, &major);\n"
"glGetIntegerv(GL_MINOR_VERSION, &minor);\n"
"bool has_compute = (major > 4) || (major == 4 && minor >= 3);"
);
}
} // namespace gallery
@@ -1,127 +0,0 @@
// Demo de gl_texture_load (cpp/functions/gfx/gl_texture_load.{h,cpp}).
// Carga assets/sample.png y lo muestra con ImGui::Image. Sliders para tint
// RGB que se aplican como modulacion (ImGui::Image acepta tint_col).
//
// Limitacion: el "zoom UV" se simula moviendo uv0/uv1 (que ImGui::Image acepta
// nativamente). Asi evitamos compilar un shader custom adicional para la demo.
#include "demos.h"
#include "demo.h"
#include "gfx/gl_texture_load.h"
#include "gfx/gl_loader.h"
#include <imgui.h>
#include <cstdio>
#include <cstring>
namespace gallery {
namespace {
struct TextureState {
fn::GlTexture tex{};
bool tried_load = false;
std::string_view err;
char err_buf[256] = {0};
float tint[3] = {1.0f, 1.0f, 1.0f};
float zoom = 1.0f; // 1.0 = sin zoom; >1 hace UV mas pequeno
};
TextureState& state() {
static TextureState s;
return s;
}
// Resuelve un path para el asset. Probamos varios candidatos relativos al cwd
// del binario (puede lanzarse desde build/ o desde la raiz del repo).
const char* resolve_sample_path() {
static const char* candidates[] = {
"assets/sample.png",
"apps/primitives_gallery/assets/sample.png",
"cpp/apps/primitives_gallery/assets/sample.png",
"../cpp/apps/primitives_gallery/assets/sample.png",
"../../cpp/apps/primitives_gallery/assets/sample.png",
"../../../cpp/apps/primitives_gallery/assets/sample.png",
nullptr,
};
for (int i = 0; candidates[i]; i++) {
FILE* f = std::fopen(candidates[i], "rb");
if (f) { std::fclose(f); return candidates[i]; }
}
return candidates[0]; // devolver el primer candidato para que el error sea mas descriptivo
}
} // namespace
void demo_gl_texture() {
demo_header("gl_texture_load", "v1.0.0",
"Carga PNG/JPG/HDR desde disco a una textura GL lista para sampler2D. "
"Vendorea stb_image (cpp/vendor/stb/). Demo: assets/sample.png "
"(damero 256x256), tint RGB modulando ImGui::Image, zoom UV.");
auto& s = state();
if (!s.tried_load) {
// Asegurar simbolos GL resueltos (Linux no-op, Windows wglGetProcAddress).
fn::gfx::gl_loader_init();
const char* path = resolve_sample_path();
s.tex = fn::gl_texture_load(path, /*flip_y=*/true, /*srgb=*/false);
if (!s.tex.ok()) {
std::snprintf(s.err_buf, sizeof(s.err_buf),
"no se pudo cargar '%s': %s",
path, fn::gl_texture_last_error());
}
s.tried_load = true;
}
if (!s.tex.ok()) {
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", s.err_buf);
ImGui::TextWrapped(
"El binario busca el PNG en varios paths relativos al cwd. "
"Lanzar desde la raiz del repo o desde cpp/build/ deberia funcionar.");
return;
}
section("Texture info");
ImGui::Text("size: %d x %d px", s.tex.w, s.tex.h);
ImGui::Text("channels: %d (forzado a RGBA en upload)", s.tex.channels);
ImGui::Text("gl_id: %u", (unsigned)s.tex.id);
section("Tint + zoom");
ImGui::SliderFloat3("tint RGB", s.tint, 0.0f, 2.0f, "%.2f");
ImGui::SliderFloat("zoom UV", &s.zoom, 0.25f, 4.0f, "%.2fx");
section("Preview");
// Calcular UVs centradas con zoom: 1.0 = (0,0)-(1,1), 2.0 = (0.25,0.25)-(0.75,0.75)
float u_half = 0.5f / (s.zoom > 0.001f ? s.zoom : 0.001f);
ImVec2 uv0(0.5f - u_half, 0.5f - u_half);
ImVec2 uv1(0.5f + u_half, 0.5f + u_half);
ImVec4 tint(s.tint[0], s.tint[1], s.tint[2], 1.0f);
// Conversion GLuint -> ImTextureID. ImGui::Image acepta cualquier id de
// textura del backend; en imgui_impl_opengl3 es directamente el GLuint.
ImTextureID tid = (ImTextureID)(intptr_t)s.tex.id;
ImGui::ImageWithBg(tid, ImVec2(384.0f, 384.0f), uv0, uv1,
ImVec4(0, 0, 0, 0), tint);
code_block(
"#include \"gfx/gl_texture_load.h\"\n\n"
"auto tex = fn::gl_texture_load(\"assets/sample.png\");\n"
"if (!tex.ok()) {\n"
" fprintf(stderr, \"%s\\n\", fn::gl_texture_last_error());\n"
" return 1;\n"
"}\n"
"// uso en shader:\n"
"glUseProgram(prog);\n"
"fn::gl_texture_bind_uniform(prog, \"u_tex\", tex, /*unit=*/0);\n"
"glDrawArrays(GL_TRIANGLES, 0, 6);\n\n"
"// o en ImGui directamente:\n"
"ImGui::Image((ImTextureID)(intptr_t)tex.id, ImVec2(w, h));"
);
}
} // namespace gallery
-443
View File
@@ -1,443 +0,0 @@
#include "demos.h"
#include "demo.h"
#include "viz/graph_types.h"
#include "viz/graph_viewport.h"
#include "viz/graph_force_layout.h"
#include "viz/graph_force_layout_gpu.h"
#include "viz/graph_layouts.h"
#include "viz/graph_labels.h"
#include "core/button.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cmath>
#include <cstdio>
#include <vector>
namespace gallery {
// Paleta del demo: 8 colores tipo Mantine. v2.0 los usamos a traves de la
// tabla EntityType en lugar de escribirlos por nodo. Asi el modelo nuevo
// queda demostrado tal cual lo van a usar las apps reales (osint_graph,
// fn_explorer): tabla pequena de tipos + nodos que solo guardan type_id.
static const uint32_t k_demo_palette[] = {
0xFFEF8D5Bu, 0xFF8CCA58u, 0xFF3E97F5u, 0xFF5051D9u,
0xFFE07FB8u, 0xFFCCCD5Fu, 0xFF52CDF2u, 0xFF61D199u,
};
static constexpr int k_demo_palette_n =
sizeof(k_demo_palette) / sizeof(k_demo_palette[0]);
// Tabla compartida entre regeneraciones — las apariencias no cambian aunque
// el usuario regenere el grafo, asi que vive como `static`.
static EntityType s_demo_entity_types[k_demo_palette_n];
static RelationType s_demo_relation_types[1];
static bool s_demo_types_initialized = false;
static void init_demo_types() {
if (s_demo_types_initialized) return;
for (int k = 0; k < k_demo_palette_n; ++k) {
s_demo_entity_types[k] = entity_type(k_demo_palette[k],
SHAPE_CIRCLE, 4.0f, "cluster");
}
s_demo_relation_types[0] = relation_type(0xFF888888u, EDGE_SOLID, 1.0f, "default");
s_demo_types_initialized = true;
}
// Genera un grafo sintetico con N nodos en K clusters. Cada nodo tiene
// `edges_per_node` aristas intra-cluster + un pct% global inter-cluster.
// Cluster radio escala con sqrt(N) para que la "nube" no sea siempre el
// mismo cuadrado de 200 px — a 1M nodos crece a ~6 km de radio en graph
// space y los nodos pueden esparcirse libremente sin caja artificial.
static void generate_synthetic_graph(int N, int K,
int edges_per_node, int inter_pct,
std::vector<GraphNode>& nodes_out,
std::vector<GraphEdge>& edges_out) {
nodes_out.clear();
edges_out.clear();
nodes_out.reserve(N);
edges_out.reserve((size_t)N * (size_t)edges_per_node + (size_t)N * (size_t)inter_pct / 100u);
unsigned seed = 0x1234abcd;
auto rnd = [&]() {
seed = seed * 1664525u + 1013904223u;
return static_cast<float>((seed >> 8) & 0xffffff) / 16777216.0f;
};
// Cluster radius y scatter escalan con sqrt(N) para que los nodos no
// queden empaquetados al subir el slider. A 1M nodes el espacio inicial
// es ~12k px de lado en lugar de los 280 px hardcoded de antes.
const float scale = std::sqrt(static_cast<float>(std::max(N, 1)));
const float cluster_r = 12.0f * scale;
const float scatter = 4.0f * scale;
std::vector<float> cluster_cx(K), cluster_cy(K);
for (int k = 0; k < K; k++) {
float angle = 2.0f * 3.14159f * k / K;
cluster_cx[k] = std::cos(angle) * cluster_r;
cluster_cy[k] = std::sin(angle) * cluster_r;
}
for (int i = 0; i < N; i++) {
int k = i % K;
// type_id mapea al EntityType (k % k_demo_palette_n) que define
// color y shape. size_override = 3..5 px para conservar la
// variacion sutil del demo v1 — apariencia visual identica.
uint16_t tid = static_cast<uint16_t>(k % k_demo_palette_n);
GraphNode n = graph_node(
cluster_cx[k] + (rnd() - 0.5f) * scatter,
cluster_cy[k] + (rnd() - 0.5f) * scatter,
tid);
n.size_override = 3.0f + rnd() * 2.0f;
n.user_data = static_cast<uint64_t>(i);
nodes_out.push_back(n);
}
auto add_edge = [&](uint32_t a, uint32_t b, float w) {
if (a == b) return;
edges_out.push_back(graph_edge(a, b, w));
};
int per_cluster = N / K;
for (int k = 0; k < K; k++) {
int base = k * per_cluster;
int end = (k == K - 1) ? N : (base + per_cluster);
int size = end - base;
if (size < 2) continue;
for (int i = base; i < end; i++) {
for (int e = 0; e < edges_per_node; e++) {
int j = base + static_cast<int>(rnd() * size);
add_edge(static_cast<uint32_t>(i),
static_cast<uint32_t>(j), 1.0f);
}
}
}
// Inter-cluster: pct% del total de nodos
long long inter = (long long)N * (long long)inter_pct / 100LL;
for (long long e = 0; e < inter; e++) {
uint32_t a = static_cast<uint32_t>(rnd() * N);
uint32_t b = static_cast<uint32_t>(rnd() * N);
add_edge(a, b, 0.3f);
}
}
void demo_graph() {
demo_header("graph_viewport", "v1.0.0",
"Pipeline completo de visualizacion de grafos: graph_renderer (instanced GPU) "
"+ graph_force_layout (Barnes-Hut) + graph_spatial_hash (hit-testing). "
"Render a FBO mostrado via ImGui::Image — escala a decenas de miles de nodos.");
static int s_n_nodes = 1000;
static int s_n_clusters = 6;
static int s_edges_per_n = 3; // aristas intra-cluster por nodo
static int s_inter_pct = 5; // % de nodos para edges inter-cluster
static float s_repulsion = 3500.0f; // fuerza de dispersion entre nodos
static float s_attraction = 0.02f; // muelle entre nodos conectados
static float s_gravity = 0.001f; // tiron hacia el centro
static std::vector<GraphNode> s_nodes;
static std::vector<GraphEdge> s_edges;
static GraphData s_graph{};
static GraphViewportState s_state;
static bool s_initialized = false;
static bool s_needs_regen = true;
// GPU layout (issue 0049h): toggle CPU/GPU. ctx se crea perezosamente al
// primer frame en GPU mode; max_nodes/max_edges se dimensionan al maximo
// que ofrece el slider (1M nodos x 10 edges/nodo = 10M edges) — los SSBOs
// ocupan ~80 MB en ese tope, suficientemente barato para no
// recrear el ctx cada Regenerate. Si compute no esta disponible, el
// toggle queda deshabilitado.
static bool s_use_gpu = false;
static ForceLayoutGPU* s_gpu_ctx = nullptr;
static bool s_gpu_dirty = true; // re-upload tras regen / cambio
// Layout estatico activo (issue 0049i). 0=force (iterativo), 1=grid,
// 2=circular, 3=radial, 4=hierarchical, 5=fixed.
static int s_layout_mode = 0;
const char* k_layout_names[] = {
"force", "grid", "circular", "radial", "hierarchical", "fixed"
};
static int s_apply_layout = 0; // se incrementa cuando hay que reaplicar
// Labels (issue 0049j). LabelPolicy controlable desde la UI.
static graph::LabelPolicy s_label_policy;
static bool s_labels_enabled = true;
if (s_needs_regen) {
init_demo_types();
generate_synthetic_graph(s_n_nodes, s_n_clusters,
s_edges_per_n, s_inter_pct,
s_nodes, s_edges);
s_graph.nodes = s_nodes.data();
s_graph.node_count = static_cast<int>(s_nodes.size());
s_graph.node_capacity = static_cast<int>(s_nodes.capacity());
s_graph.edges = s_edges.data();
s_graph.edge_count = static_cast<int>(s_edges.size());
s_graph.edge_capacity = static_cast<int>(s_edges.capacity());
s_graph.types = s_demo_entity_types;
s_graph.type_count = k_demo_palette_n;
s_graph.rel_types = s_demo_relation_types;
s_graph.rel_type_count = 1;
s_graph.update_bounds();
s_state.layout_running = true;
s_state.layout_energy = 0.0f;
s_needs_regen = false;
s_initialized = true;
s_gpu_dirty = true;
}
section("Controls");
{
using namespace fn_ui;
ImGui::PushItemWidth(180);
// Slider Nodes con escala logaritmica para que sea util tanto a 100
// como a 1M sin tener que arrastrar 10000px.
ImGui::SliderInt("Nodes", &s_n_nodes, 100, 1000000, "%d",
ImGuiSliderFlags_Logarithmic);
ImGui::SameLine();
ImGui::SliderInt("Clusters", &s_n_clusters, 2, 16);
ImGui::SliderInt("Edges/node", &s_edges_per_n, 1, 10);
ImGui::SameLine();
ImGui::SliderInt("Inter %", &s_inter_pct, 0, 30, "%d%%");
ImGui::SliderFloat("Repulsion", &s_repulsion, 100.0f, 20000.0f, "%.0f");
ImGui::SameLine();
ImGui::SliderFloat("Attraction", &s_attraction, 0.001f, 0.5f, "%.3f");
ImGui::SameLine();
ImGui::SliderFloat("Gravity", &s_gravity, 0.0f, 0.05f, "%.4f");
ImGui::PopItemWidth();
if (button("Regenerate", ButtonVariant::Primary)) s_needs_regen = true;
ImGui::SameLine();
if (button(s_state.layout_running ? "Pause layout" : "Resume layout",
ButtonVariant::Secondary)) {
s_state.layout_running = !s_state.layout_running;
}
ImGui::SameLine();
if (button("Fit view", ButtonVariant::Subtle)) {
graph_viewport_fit(s_graph, s_state);
}
ImGui::SameLine();
// Toggle GPU layout. Si compute no esta disponible (Mesa software o
// driver < 4.3), deshabilitamos visualmente el checkbox.
bool prev_gpu = s_use_gpu;
if (s_gpu_ctx == nullptr && s_use_gpu == false) {
// primera oportunidad: intentar crear el ctx para detectar soporte.
// Lazy init solo si el usuario lo activa.
}
ImGui::Checkbox("GPU layout", &s_use_gpu);
if (s_use_gpu != prev_gpu) {
s_gpu_dirty = true; // re-upload al cambiar de modo
}
// Selector de layout (issue 0049i).
ImGui::PushItemWidth(140);
int prev_mode = s_layout_mode;
if (ImGui::Combo("Layout", &s_layout_mode,
k_layout_names, IM_ARRAYSIZE(k_layout_names))) {
// Cambio de modo: reaplicar instantaneamente
s_apply_layout++;
}
if (prev_mode != s_layout_mode) {
// En "force" volvemos a animar; en cualquier estatico paramos.
s_state.layout_running = (s_layout_mode == 0);
}
ImGui::PopItemWidth();
ImGui::SameLine();
if (button("Apply layout", ButtonVariant::Subtle)) s_apply_layout++;
// --- Labels (issue 0049j) ---------------------------------------
ImGui::Checkbox("Labels", &s_labels_enabled);
ImGui::SameLine();
ImGui::PushItemWidth(140);
ImGui::SliderInt("Max visible", &s_label_policy.max_visible, 0, 1000);
ImGui::SameLine();
ImGui::SliderFloat("Font", &s_label_policy.font_size,
8.0f, 24.0f, "%.0f");
ImGui::SameLine();
ImGui::SliderFloat("Min px", &s_label_policy.min_node_pixel_size,
0.0f, 40.0f, "%.0f");
ImGui::PopItemWidth();
ImGui::SameLine();
ImGui::Checkbox("Selected", &s_label_policy.always_for_selected);
ImGui::SameLine();
ImGui::Checkbox("Hovered", &s_label_policy.always_for_hovered);
ImGui::SameLine();
ImGui::Checkbox("Pinned", &s_label_policy.always_for_pinned);
}
section("Stats");
{
// Una sola linea fija — sin secciones condicionales que cambien la
// altura del panel (eso provocaba que el viewport saltara al hacer
// hover/select).
char hover_buf[32];
char sel_buf[32];
if (s_state.hovered_node >= 0) {
std::snprintf(hover_buf, sizeof(hover_buf), "#%d t%u",
s_state.hovered_node,
(unsigned)s_nodes[s_state.hovered_node].type_id);
} else {
std::snprintf(hover_buf, sizeof(hover_buf), "-");
}
if (s_state.selected_node >= 0) {
std::snprintf(sel_buf, sizeof(sel_buf), "#%d", s_state.selected_node);
} else {
std::snprintf(sel_buf, sizeof(sel_buf), "-");
}
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::Text("nodes=%d edges=%d energy=%.2f fps=%.0f | hover=%s sel=%s",
s_graph.node_count, s_graph.edge_count,
s_state.layout_energy, ImGui::GetIO().Framerate,
hover_buf, sel_buf);
ImGui::PopStyleColor();
}
// Aplicar layout estatico cuando se solicita (cambio de modo / boton).
static int s_last_apply = -1;
if (s_apply_layout != s_last_apply) {
s_last_apply = s_apply_layout;
switch (s_layout_mode) {
case 1: graph::layout_grid (s_graph, 25.0f); break;
case 2: graph::layout_circular (s_graph, 200.0f); break;
case 3: graph::layout_radial (s_graph, 0, 80.0f); break;
case 4: graph::layout_hierarchical(s_graph, 0, 120.0f, 50.0f); break;
case 5: graph::layout_fixed (s_graph); break;
case 0: default:
// force: dejar las posiciones actuales; el bucle lo refinara
break;
}
s_gpu_dirty = true;
if (s_layout_mode != 0) graph_viewport_fit(s_graph, s_state);
}
section("Viewport (drag=pan, wheel=zoom, click=select, shift+drag=lasso, ctrl+click=toggle)");
if (s_initialized) {
// Avanzamos 1 paso de force layout cada frame mientras layout_running.
// Auto-pause: si la energia por nodo cae bajo el umbral durante N
// frames consecutivos, paramos la simulacion automaticamente — el
// grafo ya esta estable. El usuario lo retoma con "Resume layout"
// o "Regenerate".
static int s_low_energy_frames = 0;
const int k_pause_after_frames = 30;
const float k_pause_per_node = 0.001f; // umbral de energia/nodo
if (s_state.layout_running && s_layout_mode == 0) {
ForceLayoutConfig cfg;
cfg.repulsion = s_repulsion;
cfg.attraction = s_attraction;
cfg.gravity = s_gravity;
cfg.iterations = 1;
if (s_use_gpu) {
if (!s_gpu_ctx) {
s_gpu_ctx = graph_force_layout_gpu_create(s_graph.node_count + 1024,
s_graph.edge_count + 1024);
s_gpu_dirty = true;
}
if (s_gpu_ctx) {
if (s_gpu_dirty) {
graph_force_layout_gpu_upload(s_gpu_ctx, s_graph);
s_gpu_dirty = false;
}
s_state.layout_energy = graph_force_layout_gpu_step(s_gpu_ctx, cfg);
graph_force_layout_gpu_readback(s_gpu_ctx, s_graph, /*include_velocities=*/true);
} else {
// GPU no disponible: caer a CPU silenciosamente.
s_use_gpu = false;
s_state.layout_energy = graph_force_layout_step(s_graph, cfg);
}
} else {
s_state.layout_energy = graph_force_layout_step(s_graph, cfg);
}
const float per_node = s_graph.node_count > 0
? s_state.layout_energy / (float)s_graph.node_count
: 0.0f;
if (per_node < k_pause_per_node) ++s_low_energy_frames;
else s_low_energy_frames = 0;
if (graph_force_layout_should_pause(s_low_energy_frames,
k_pause_after_frames)) {
s_state.layout_running = false;
s_low_energy_frames = 0;
}
} else {
s_low_energy_frames = 0;
}
// Callbacks (issue 0049i): right-click abre popup contextual,
// double-click loguea el indice. Los callbacks corren dentro del
// frame ImGui — el caller puede usar OpenPopup directamente.
static int s_ctx_node = -1;
static bool s_ctx_open = false;
struct Cb {
static void on_ctx(int idx, ImVec2 /*pos*/, void* user) {
int* slot = (int*)user;
*slot = idx;
ImGui::OpenPopup("##graph_node_ctx");
}
static void on_dbl(int idx, void* /*user*/) {
std::printf("[graph] dbl-click on node %d\n", idx);
}
};
GraphViewportCallbacks cb;
cb.on_context_menu = &Cb::on_ctx;
cb.on_double_click = &Cb::on_dbl;
cb.user = &s_ctx_node;
graph_viewport("##graph_demo", s_graph, s_state, ImVec2(0, 460), cb);
// Labels overlay (issue 0049j). El callback formatea "#<idx>" en un
// buffer estatico por demo — apps reales (osint_graph) usaran un
// string pool de la BD origen.
if (s_labels_enabled) {
struct LblCtx { char buf[32]; };
static LblCtx s_lbl_ctx;
auto get_label = [](int idx, void* user) -> const char* {
auto* ctx = static_cast<LblCtx*>(user);
std::snprintf(ctx->buf, sizeof(ctx->buf), "#%d", idx);
return ctx->buf;
};
graph::graph_labels_draw(s_graph, s_state, s_label_policy,
get_label, &s_lbl_ctx);
}
if (ImGui::BeginPopup("##graph_node_ctx")) {
ImGui::Text("Node #%d", s_ctx_node);
ImGui::Separator();
if (ImGui::MenuItem("Pin")) {
if (s_ctx_node >= 0 && s_ctx_node < s_graph.node_count)
s_graph.nodes[s_ctx_node].flags |= NF_PINNED;
}
if (ImGui::MenuItem("Unpin")) {
if (s_ctx_node >= 0 && s_ctx_node < s_graph.node_count)
s_graph.nodes[s_ctx_node].flags &= ~NF_PINNED;
}
if (ImGui::MenuItem("Add to selection")) {
graph_viewport_add_to_selection(s_graph, s_state, s_ctx_node);
}
ImGui::EndPopup();
}
// Overlay con count seleccionados (lasso/multi-select feedback).
if (!s_state.selection.empty()) {
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text);
ImGui::Text("[%zu selected]", s_state.selection.size());
ImGui::PopStyleColor();
}
}
code_block(
"static GraphData graph;\n"
"static GraphViewportState state;\n"
"// ... rellenar graph.nodes / graph.edges ...\n"
"graph.update_bounds();\n"
"\n"
"// Por frame:\n"
"if (state.layout_running) {\n"
" ForceLayoutConfig cfg;\n"
" cfg.repulsion = 3500; cfg.gravity = 0.001f;\n"
" graph_force_layout_step(graph, cfg);\n"
"}\n"
"graph_viewport(\"##g\", graph, state, ImVec2(0, 460));"
);
}
} // namespace gallery
@@ -1,243 +0,0 @@
#include "demos.h"
#include "demo.h"
#include "viz/graph_types.h"
#include "viz/graph_viewport.h"
#include "viz/graph_renderer.h"
#include "viz/graph_force_layout.h"
#include "viz/graph_icons.h"
#include "core/button.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cmath>
#include <cstdio>
#include <vector>
namespace gallery {
// 6 codepoints Tabler representativos para los 6 EntityTypes del demo. El
// orden coincide con `s_entity_types[i]`: cada tipo apunta a `icon_id = i+1`
// (las regiones del atlas son 1-indexed; 0 reservado para "sin icono").
static const uint16_t k_demo_codepoints[6] = {
0xEB4Du, // TI_USER
0xEAE5u, // TI_MAIL
0xEAB9u, // TI_GLOBE
0xEB09u, // TI_PHONE
0xEA4Fu, // TI_BUILDING
0xEA88u, // TI_DATABASE
};
static const uint32_t k_styles_palette[6] = {
0xFF6BCB77u, // verde — Person (circle)
0xFFFF6B6Bu, // rojo — Email (square)
0xFF4D96FFu, // azul — Domain (diamond)
0xFFFFC75Fu, // ambar — Phone (hex)
0xFFC780E8u, // morado — Org (triangle)
0xFF52CDF2u, // cyan — Database (rounded square)
};
static const char* k_styles_names[6] = {
"Person", "Email", "Domain", "Phone", "Organization", "Database"
};
static EntityType s_entity_types[6];
static RelationType s_relation_types[3]; // solid, dashed, dotted
static IconAtlas* s_atlas = nullptr;
static bool s_types_initialized = false;
static bool s_atlas_bound = false;
static void init_demo_types() {
if (s_types_initialized) return;
for (int i = 0; i < 6; ++i) {
EntityType t{};
t.color = k_styles_palette[i];
t.shape = (uint8_t)(SHAPE_CIRCLE + i); // 1..6 — uno por shape
t.icon_id = (uint16_t)(i + 1); // 1-based
t.default_size = 14.0f;
t.name = k_styles_names[i];
s_entity_types[i] = t;
}
s_relation_types[0] = relation_type(0xFFCCCCCCu, EDGE_SOLID, 1.5f, "knows");
s_relation_types[1] = relation_type(0xFFFFB870u, EDGE_DASHED, 1.5f, "uses");
s_relation_types[2] = relation_type(0xFF89E0FCu, EDGE_DOTTED, 1.5f, "owns");
s_types_initialized = true;
}
// 30 nodos posicionados en un anillo por tipo. Aristas: cada nodo conecta a
// sus dos vecinos (arc) y a un nodo "central" del cluster siguiente. Mezcla
// de directed/undirected para validar las flechas.
static void build_demo_graph(std::vector<GraphNode>& nodes,
std::vector<GraphEdge>& edges)
{
nodes.clear();
edges.clear();
const int per_type = 5;
const float ring_r = 80.0f;
const float type_r = 30.0f;
for (int t = 0; t < 6; ++t) {
float ang_t = (float)t * (2.0f * 3.14159265f / 6.0f);
float cx = std::cos(ang_t) * ring_r;
float cy = std::sin(ang_t) * ring_r;
for (int k = 0; k < per_type; ++k) {
float a = (float)k * (2.0f * 3.14159265f / per_type) + ang_t * 0.3f;
GraphNode n = graph_node(cx + std::cos(a) * type_r,
cy + std::sin(a) * type_r,
(uint16_t)t);
n.user_data = (uint64_t)nodes.size();
nodes.push_back(n);
}
}
auto idx = [&](int t, int k) { return (uint32_t)(t * per_type + k); };
for (int t = 0; t < 6; ++t) {
// Aristas intra-cluster (knows = solid, undirected).
for (int k = 0; k < per_type; ++k) {
int next_k = (k + 1) % per_type;
GraphEdge e = graph_edge(idx(t, k), idx(t, next_k), 1.0f, /*type_id=*/0);
edges.push_back(e);
}
// Inter-cluster: del nodo 0 del cluster t al nodo 0 del cluster t+1
// como "uses" (dashed, directed).
int t_next = (t + 1) % 6;
GraphEdge e1 = graph_edge(idx(t, 0), idx(t_next, 0), 1.0f, /*type_id=*/1);
e1.flags |= EF_DIRECTED;
edges.push_back(e1);
// Y otra inter-cluster mas larga al cluster +2 como "owns" (dotted,
// directed). Asi se ven las 3 estilos a la vez.
int t_far = (t + 2) % 6;
GraphEdge e2 = graph_edge(idx(t, 2), idx(t_far, 3), 0.6f, /*type_id=*/2);
e2.flags |= EF_DIRECTED;
edges.push_back(e2);
}
}
void demo_graph_styles() {
demo_header("graph_renderer (shapes + icons + arrows + edge styles)", "v1.5.0",
"OSINT-style: 6 EntityTypes, uno por shape (circle, square, diamond, hex, "
"triangle, rounded square) con icono Tabler en el centro. 3 RelationTypes "
"(solid/dashed/dotted) con flechas en los aristas EF_DIRECTED. Mismas dos "
"draw calls que el viewport normal (1 nodos + 1 aristas).");
init_demo_types();
static std::vector<GraphNode> s_nodes;
static std::vector<GraphEdge> s_edges;
static GraphData s_graph{};
static GraphViewportState s_state;
static bool s_initialized = false;
static bool s_run_layout = false;
if (!s_initialized) {
build_demo_graph(s_nodes, s_edges);
s_graph.nodes = s_nodes.data();
s_graph.node_count = (int)s_nodes.size();
s_graph.node_capacity = (int)s_nodes.capacity();
s_graph.edges = s_edges.data();
s_graph.edge_count = (int)s_edges.size();
s_graph.edge_capacity = (int)s_edges.capacity();
s_graph.types = s_entity_types;
s_graph.type_count = 6;
s_graph.rel_types = s_relation_types;
s_graph.rel_type_count = 3;
s_graph.update_bounds();
s_state.layout_running = false; // queremos ver las shapes posicionadas, no el caos del force
s_state.zoom = 2.0f;
s_initialized = true;
}
section("Legend");
{
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
for (int i = 0; i < 6; ++i) {
ImGui::Text("%-13s shape=%d icon_id=%d color=#%06x",
k_styles_names[i],
(int)s_entity_types[i].shape,
(int)s_entity_types[i].icon_id,
(unsigned)(s_entity_types[i].color & 0x00FFFFFFu));
}
ImGui::Text("Edges: knows=solid, uses=dashed (directed), owns=dotted (directed)");
ImGui::PopStyleColor();
}
section("Controls");
{
using namespace fn_ui;
if (button(s_run_layout ? "Pause force layout" : "Run force layout",
ButtonVariant::Secondary)) {
s_run_layout = !s_run_layout;
s_state.layout_running = s_run_layout;
}
ImGui::SameLine();
if (button("Rebuild", ButtonVariant::Subtle)) {
build_demo_graph(s_nodes, s_edges);
s_graph.nodes = s_nodes.data();
s_graph.node_count = (int)s_nodes.size();
s_graph.edges = s_edges.data();
s_graph.edge_count = (int)s_edges.size();
s_graph.update_bounds();
}
ImGui::SameLine();
if (button("Fit view", ButtonVariant::Subtle)) {
graph_viewport_fit(s_graph, s_state);
}
}
section("Viewport");
if (s_run_layout) {
ForceLayoutConfig cfg;
cfg.repulsion = 1500.0f;
cfg.attraction = 0.04f;
cfg.gravity = 0.005f;
cfg.iterations = 1;
graph_force_layout_step(s_graph, cfg);
}
// El viewport crea internamente el GraphRenderer. La primera vez que se
// dibuja el panel, el renderer existe — bindeamos el atlas justo despues.
graph_viewport("##graph_styles", s_graph, s_state, ImVec2(0, 460));
if (!s_atlas_bound && s_state.renderer) {
s_atlas = graph_icons_build(k_demo_codepoints, 6, 32);
if (s_atlas) {
graph_renderer_set_icon_atlas(s_state.renderer,
graph_icons_texture(s_atlas),
graph_icons_uv_table(s_atlas),
graph_icons_count(s_atlas));
s_atlas_bound = true;
} else {
// Sin atlas: marcamos como bound para no reintentar cada frame —
// el renderer simplemente pinta sin overlay de iconos.
s_atlas_bound = true;
}
}
code_block(
"// Build atlas con 6 codepoints Tabler\n"
"const uint16_t cps[] = {0xEB4D, 0xEAE5, 0xEAB9, 0xEB09, 0xEA4F, 0xEA88};\n"
"IconAtlas* atlas = graph_icons_build(cps, 6, 32);\n"
"\n"
"// EntityTypes: cada uno con su shape e icono\n"
"EntityType person = {0xFF6BCB77, SHAPE_CIRCLE, /*icon_id=*/1, 14, \"Person\"};\n"
"EntityType email = {0xFFFF6B6B, SHAPE_SQUARE, /*icon_id=*/2, 14, \"Email\"};\n"
"// ... etc\n"
"\n"
"// RelationTypes: solid / dashed / dotted\n"
"RelationType knows = relation_type(0xFFCCCCCC, EDGE_SOLID, 1.5f, \"knows\");\n"
"RelationType uses = relation_type(0xFFFFB870, EDGE_DASHED, 1.5f, \"uses\");\n"
"\n"
"// Bind atlas al renderer\n"
"graph_renderer_set_icon_atlas(renderer, graph_icons_texture(atlas),\n"
" graph_icons_uv_table(atlas),\n"
" graph_icons_count(atlas));\n"
"\n"
"// Aristas direccionales\n"
"GraphEdge e = graph_edge(src, tgt, 1.0f, /*type_id=*/1);\n"
"e.flags |= EF_DIRECTED;");
}
} // namespace gallery
-108
View File
@@ -1,108 +0,0 @@
// Demo del primitivo viz/mesh_viewer.
// Genera un cubo procedural in-line, lo sube al GPU, y permite cargar un
// .obj desde un path ingresado en un text input.
#include "demos.h"
#include "demo.h"
#include "viz/mesh_viewer.h"
#include "gfx/mesh_obj_load.h"
#include "gfx/mesh_gpu.h"
#include "core/orbit_camera.h"
#include <imgui.h>
#include <cstring>
#include <string>
namespace gallery {
namespace {
const char* kCubeObj =
"v -1 -1 -1\nv 1 -1 -1\nv 1 1 -1\nv -1 1 -1\n"
"v -1 -1 1\nv 1 -1 1\nv 1 1 1\nv -1 1 1\n"
"f 4 3 2 1\n" // back (-Z) — winding for outward normal
"f 5 6 7 8\n" // front (+Z)
"f 1 2 6 5\n" // bottom (-Y)
"f 8 7 3 4\n" // top (+Y)
"f 5 8 4 1\n" // left (-X)
"f 2 3 7 6\n"; // right (+X)
struct State {
fn::gfx::MeshGpu mesh{};
fn::core::OrbitCamera cam{};
char path[512] = "";
std::string status;
bool wireframe = false;
bool initialized = false;
};
State& state() {
static State s;
return s;
}
void load_cube() {
auto& s = state();
if (s.mesh.ok()) fn::gfx::mesh_gpu_destroy(s.mesh);
auto cpu = fn::gfx::mesh_obj_parse(kCubeObj, std::strlen(kCubeObj));
s.mesh = fn::gfx::mesh_gpu_upload(cpu);
s.status = s.mesh.ok()
? ("loaded cube: " + std::to_string(s.mesh.index_count / 3) + " tris")
: "cube upload failed";
}
void load_from_path() {
auto& s = state();
if (!s.path[0]) { s.status = "path is empty"; return; }
auto cpu = fn::gfx::mesh_obj_load(s.path);
if (cpu.positions.empty()) { s.status = "parse/read failed"; return; }
if (s.mesh.ok()) fn::gfx::mesh_gpu_destroy(s.mesh);
s.mesh = fn::gfx::mesh_gpu_upload(cpu);
s.status = s.mesh.ok()
? ("loaded: " + std::to_string(s.mesh.index_count / 3) + " tris")
: "upload failed";
}
} // namespace
void demo_mesh_viewer() {
demo_header("mesh_viewer", "v1.0.0",
"Visualizador 3D para inspeccion de geometria. Composicion de "
"mesh_obj_load (parser .obj puro) + mesh_gpu (upload VAO/VBO/EBO) + "
"orbit_camera (drag/wheel) + mesh_viewer (FBO + ImGui::Image + Lambert).");
auto& s = state();
if (!s.initialized) {
load_cube();
s.initialized = true;
}
// Controls row.
if (ImGui::Button("Reload cube")) load_cube();
ImGui::SameLine();
ImGui::Checkbox("Wireframe", &s.wireframe);
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
ImGui::SetNextItemWidth(360);
ImGui::InputTextWithHint("##obj_path", "absolute path to .obj", s.path, sizeof(s.path));
ImGui::SameLine();
if (ImGui::Button("Load .obj")) load_from_path();
ImGui::TextDisabled("status: %s | tris: %d | drag to orbit, wheel to zoom",
s.status.c_str(),
s.mesh.ok() ? s.mesh.index_count / 3 : 0);
ImGui::Separator();
fn::viz::MeshViewerConfig cfg{};
cfg.mesh = &s.mesh;
cfg.cam = &s.cam;
cfg.size = ImVec2(-1.0f, 480.0f);
cfg.color = IM_COL32(160, 200, 255, 255);
cfg.wireframe = s.wireframe;
fn::viz::mesh_viewer("##gallery_mesh_viewer", cfg);
}
} // namespace gallery
@@ -1,208 +0,0 @@
// demos_scientific.cpp — demos para los 5 charts cientificos del issue 0034:
// treemap, sankey, chord, contour, voronoi.
#include "demos.h"
#include "demo.h"
#include "viz/treemap.h"
#include "viz/sankey.h"
#include "viz/chord.h"
#include "viz/contour.h"
#include "viz/voronoi.h"
#include <imgui.h>
#include <cmath>
#include <cstdlib>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// treemap
// ---------------------------------------------------------------------------
void demo_treemap() {
demo_header("treemap", "v1.0.0",
"Squarified treemap (Bruls et al.) para jerarquias planas con valores. "
"Algoritmo puro separado del render.");
section("Gastos por categoria");
{
std::vector<TreemapItem> items = {
{"vivienda", 950.0f, IM_COL32(180, 120, 200, 255)},
{"comida", 320.0f, IM_COL32(120, 180, 200, 255)},
{"transporte", 180.0f, IM_COL32(200, 180, 120, 255)},
{"ocio", 140.0f, IM_COL32(200, 120, 160, 255)},
{"salud", 90.0f, IM_COL32(120, 200, 160, 255)},
{"otros", 60.0f, IM_COL32(160, 160, 200, 255)},
};
treemap("##gastos", items, ImVec2(-1, 320));
}
code_block(
"std::vector<TreemapItem> items = {\n"
" {\"vivienda\", 950.0f, IM_COL32(180,120,200,255)},\n"
" {\"comida\", 320.0f, IM_COL32(120,180,200,255)},\n"
" ...\n"
"};\n"
"treemap(\"##gastos\", items, ImVec2(-1, 320));"
);
}
// ---------------------------------------------------------------------------
// sankey
// ---------------------------------------------------------------------------
void demo_sankey() {
demo_header("sankey", "v1.0.0",
"Sankey diagram para flujos source -> target. BFS topologico para columnas, "
"bandas curvas (bezier cubico) para los links. Asume DAG.");
section("Clientes -> productos -> categorias");
{
std::vector<SankeyNode> nodes = {
{"premium"}, {"basicos"},
{"laptops"}, {"phones"}, {"tablets"},
{"hardware"}, {"software"}, {"servicios"},
};
std::vector<SankeyLink> links = {
// clientes -> productos
{0, 2, 80}, {0, 3, 30}, {0, 4, 15},
{1, 3, 60}, {1, 4, 40}, {1, 2, 20},
// productos -> categorias
{2, 5, 70}, {2, 6, 30},
{3, 5, 50}, {3, 7, 40},
{4, 6, 35}, {4, 7, 20},
};
sankey("##flow", nodes, links, ImVec2(-1, 360));
}
code_block(
"std::vector<SankeyNode> nodes = {{\"premium\"}, {\"basicos\"}, ...};\n"
"std::vector<SankeyLink> links = {{0, 2, 80}, {0, 3, 30}, ...};\n"
"sankey(\"##flow\", nodes, links, ImVec2(-1, 360));"
);
}
// ---------------------------------------------------------------------------
// chord
// ---------------------------------------------------------------------------
void demo_chord() {
demo_header("chord", "v1.0.0",
"Chord diagram para matrices NxN. Arcos proporcionales a sum(row) + cuerdas "
"internas con bezier cubico.");
section("Flujos entre paises (matriz 6x6 simetrica)");
{
constexpr int N = 6;
// simetrica de "comercio" entre 6 paises
static float M[N * N] = {
0, 10, 6, 12, 4, 3,
10, 0, 14, 3, 8, 2,
6, 14, 0, 9, 11, 5,
12, 3, 9, 0, 7, 6,
4, 8, 11, 7, 0, 13,
3, 2, 5, 6, 13, 0,
};
static const char* labels[N] = {"ESP", "FRA", "ITA", "DEU", "PRT", "GBR"};
chord("##chord", M, N, labels, ImVec2(420, 420));
}
code_block(
"float M[N*N] = { // simetrica\n"
" 0, 10, 6, 12, 4, 3,\n"
" 10, 0, 14, 3, 8, 2,\n"
" ...\n"
"};\n"
"const char* labels[6] = {\"ESP\",\"FRA\",\"ITA\",\"DEU\",\"PRT\",\"GBR\"};\n"
"chord(\"##c\", M, 6, labels);"
);
}
// ---------------------------------------------------------------------------
// contour
// ---------------------------------------------------------------------------
void demo_contour() {
demo_header("contour", "v1.0.0",
"Contour plot 2D via marching squares. Para una gaussiana centrada los "
"contornos resultantes son aproximadamente concentricos.");
constexpr int N = 32;
static float grid[N * N];
static bool init = false;
if (!init) {
// Mezcla de 2 gaussianas (peak central + secundario)
for (int y = 0; y < N; y++) {
for (int x = 0; x < N; x++) {
float dx1 = x - N * 0.45f, dy1 = y - N * 0.5f;
float dx2 = x - N * 0.75f, dy2 = y - N * 0.3f;
float v = std::exp(-(dx1 * dx1 + dy1 * dy1) / 70.0f)
+ 0.55f * std::exp(-(dx2 * dx2 + dy2 * dy2) / 30.0f);
grid[y * N + x] = v;
}
}
init = true;
}
static const float levels[] = {0.15f, 0.30f, 0.50f, 0.70f, 0.90f};
contour("##gauss", grid, N, N, levels, 5, ImVec2(-1, 320));
code_block(
"constexpr int N = 32;\n"
"float grid[N*N];\n"
"for (int y = 0; y < N; y++)\n"
" for (int x = 0; x < N; x++) {\n"
" float dx = x - N/2.0f, dy = y - N/2.0f;\n"
" grid[y*N + x] = std::exp(-(dx*dx + dy*dy) / 80.0f);\n"
" }\n"
"float levels[] = {0.15f, 0.30f, 0.50f, 0.70f, 0.90f};\n"
"contour(\"##gauss\", grid, N, N, levels, 5);"
);
}
// ---------------------------------------------------------------------------
// voronoi
// ---------------------------------------------------------------------------
void demo_voronoi() {
demo_header("voronoi", "v1.0.0",
"Diagrama de Voronoi via raster brute-force (MVP). Tiles 4x4 px coloreados "
"por el seed mas cercano. Suficiente para N <= 200.");
constexpr int N = 30;
static ImVec2 seeds [N];
static ImU32 colors[N];
static bool init = false;
if (!init) {
unsigned seed = 7;
auto rnd = [&]() {
seed = seed * 1103515245u + 12345u;
return (float)((seed >> 16) & 0x7fff) / 32768.0f;
};
// El render escala automaticamente; las posiciones se asumen en coords del rect.
// Como no sabemos W/H aqui, usamos coords aproximadas para 600x300 y el clip
// dentro de voronoi se encarga de mantenerlas en rango.
for (int i = 0; i < N; i++) {
seeds [i] = ImVec2(rnd() * 600.0f, rnd() * 300.0f);
colors[i] = IM_COL32(40 + (int)(rnd() * 200),
40 + (int)(rnd() * 200),
60 + (int)(rnd() * 195),
230);
}
init = true;
}
voronoi("##v", seeds, N, colors, ImVec2(-1, 300));
code_block(
"ImVec2 seeds[30];\n"
"ImU32 colors[30];\n"
"for (int i = 0; i < 30; i++) {\n"
" seeds [i] = ImVec2(rnd() * 600.0f, rnd() * 300.0f);\n"
" colors[i] = IM_COL32(rnd_byte(), rnd_byte(), rnd_byte(), 230);\n"
"}\n"
"voronoi(\"##v\", seeds, 30, colors, ImVec2(-1, 300));"
);
}
} // namespace gallery
-129
View File
@@ -1,129 +0,0 @@
// Demo de sql_workbench (Core, issue 0032).
//
// Abre `registry.db` en modo readonly y deja que el componente liste sus
// tablas en la sidebar. La idea es probar el ciclo Run + tabla + historial
// contra una DB real sin riesgo de mutarla.
#include "demos.h"
#include "demo.h"
#include "core/sql_workbench.h"
#include "core/tokens.h"
#include <imgui.h>
#include <sqlite3.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
namespace gallery {
namespace {
struct SqlDemoState {
sqlite3* db = nullptr;
fn::SqlWorkbenchState wb;
bool tried_open = false;
std::string db_path;
std::string open_error;
};
SqlDemoState& state() {
static SqlDemoState s;
return s;
}
// Resuelve la ruta a registry.db: env FN_REGISTRY_ROOT/registry.db si existe,
// si no, prueba ./registry.db, ../registry.db, ../../registry.db (build tree).
std::string resolve_registry_db() {
if (const char* env = std::getenv("FN_REGISTRY_ROOT")) {
std::string p = std::string(env) + "/registry.db";
if (FILE* f = std::fopen(p.c_str(), "rb")) { std::fclose(f); return p; }
}
const char* candidates[] = {
"registry.db",
"../registry.db",
"../../registry.db",
"../../../registry.db",
"../../../../registry.db",
};
for (const char* c : candidates) {
if (FILE* f = std::fopen(c, "rb")) { std::fclose(f); return c; }
}
return "";
}
void ensure_open() {
auto& s = state();
if (s.tried_open) return;
s.tried_open = true;
s.db_path = resolve_registry_db();
if (s.db_path.empty()) {
s.open_error = "registry.db not found (tried FN_REGISTRY_ROOT and parent dirs)";
return;
}
int rc = sqlite3_open_v2(s.db_path.c_str(), &s.db,
SQLITE_OPEN_READONLY, nullptr);
if (rc != SQLITE_OK) {
s.open_error = sqlite3_errmsg(s.db);
if (s.db) { sqlite3_close(s.db); s.db = nullptr; }
return;
}
s.wb.readonly = true;
// Query inicial mas util para el demo: lista de funciones del registry.
s.wb.query =
"SELECT id, kind, purity, domain\n"
"FROM functions\n"
"ORDER BY id\n"
"LIMIT 50;";
}
} // namespace
void demo_sql_workbench() {
using namespace fn_tokens;
demo_header("sql_workbench", "v1.0.0",
"Workbench SQL: editor con highlighting, schema sidebar, tabla de "
"resultados e historial. Ejecuta queries contra una sqlite3* del caller. "
"En este demo, registry.db abierto en modo readonly.");
ensure_open();
auto& s = state();
if (!s.open_error.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::error);
ImGui::TextWrapped("could not open registry.db: %s", s.open_error.c_str());
ImGui::PopStyleColor();
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextWrapped("Set FN_REGISTRY_ROOT to the repo root or run from the repo cwd.");
ImGui::PopStyleColor();
return;
}
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::Text("db: %s (readonly)", s.db_path.c_str());
ImGui::PopStyleColor();
section("workbench");
{
ImVec2 avail = ImGui::GetContentRegionAvail();
// Reserva un pelin para el code_block de abajo.
float h = avail.y - 110.0f;
if (h < 320.0f) h = 320.0f;
fn::sql_workbench("##gallery_sql", s.db, s.wb, ImVec2(-1, h));
}
code_block(
"sqlite3* db = nullptr;\n"
"sqlite3_open_v2(\"registry.db\", &db, SQLITE_OPEN_READONLY, nullptr);\n"
"fn::SqlWorkbenchState st;\n"
"st.readonly = true;\n"
"fn::sql_workbench(\"##sql\", db, st, ImVec2(-1, -1));"
);
}
} // namespace gallery
@@ -1,279 +0,0 @@
// Demos individuales de text_editor y file_watcher (Wave 1, issue 0025).
//
// Aunque las dos primitivas estan diseñadas para componerse, en gallery se
// muestran por separado para que cada entry exhiba un solo primitivo y su
// API minima.
#include "demos.h"
#include "demo.h"
#include "core/text_editor.h"
#include "core/file_watcher.h"
#include "core/button.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <deque>
#include <string>
namespace gallery {
// ===========================================================================
// text_editor — editor de codigo con syntax highlighting
// ===========================================================================
namespace {
const char* kSampleGLSL =
"#version 330\n"
"// fragment shader demo\n"
"out vec4 frag_color;\n"
"uniform vec2 u_resolution;\n"
"uniform float u_time;\n"
"\n"
"void main() {\n"
" vec2 uv = gl_FragCoord.xy / u_resolution;\n"
" vec3 col = 0.5 + 0.5 * cos(u_time + uv.xyx + vec3(0,2,4));\n"
" frag_color = vec4(col, 1.0);\n"
"}\n";
const char* kSampleSQL =
"-- fts5 search sobre el registry\n"
"SELECT id, kind, purity, description\n"
"FROM functions\n"
"WHERE id IN (\n"
" SELECT id FROM functions_fts\n"
" WHERE functions_fts MATCH 'name:slic* OR description:slic*'\n"
")\n"
"ORDER BY name\n"
"LIMIT 50;\n";
const char* kSampleCpp =
"#include <imgui.h>\n"
"namespace fn {\n"
" bool button(const char* label, ButtonVariant v) {\n"
" auto& tk = tokens::current();\n"
" ImGui::PushStyleColor(ImGuiCol_Button, tk.bg_for(v));\n"
" bool clicked = ImGui::Button(label);\n"
" ImGui::PopStyleColor();\n"
" return clicked;\n"
" }\n"
"}\n";
struct EditorState {
fn::TextEditorState* ed = nullptr;
fn::CodeLang lang = fn::CodeLang::GLSL;
};
EditorState& editor_state() {
static EditorState s;
return s;
}
void ensure_editor() {
auto& s = editor_state();
if (!s.ed) {
s.ed = fn::text_editor_create(s.lang);
fn::text_editor_set_text(s.ed, kSampleGLSL);
}
}
void apply_language(fn::CodeLang next) {
auto& s = editor_state();
if (next == s.lang) return;
fn::text_editor_destroy(s.ed);
s.ed = fn::text_editor_create(next);
s.lang = next;
switch (next) {
case fn::CodeLang::GLSL: fn::text_editor_set_text(s.ed, kSampleGLSL); break;
case fn::CodeLang::SQL: fn::text_editor_set_text(s.ed, kSampleSQL); break;
case fn::CodeLang::Cpp: fn::text_editor_set_text(s.ed, kSampleCpp); break;
case fn::CodeLang::Generic: fn::text_editor_set_text(s.ed, ""); break;
}
}
} // namespace
void demo_text_editor() {
using namespace fn_tokens;
demo_header("text_editor", "v1.0.0",
"Editor de codigo embebido en ImGui con syntax highlighting (GLSL/SQL/Cpp/Generic). "
"Wrapper PIMPL sobre ImGuiColorTextEdit (MIT). API: create / set_text / get_text / "
"render / is_dirty.");
ensure_editor();
auto& s = editor_state();
section("language");
{
const char* labels[] = {"GLSL", "SQL", "Cpp", "Generic"};
const fn::CodeLang langs[] = {
fn::CodeLang::GLSL, fn::CodeLang::SQL, fn::CodeLang::Cpp, fn::CodeLang::Generic
};
for (int i = 0; i < 4; ++i) {
if (i > 0) ImGui::SameLine();
bool active = (s.lang == langs[i]);
if (active) ImGui::PushStyleColor(ImGuiCol_Button, colors::primary);
if (ImGui::Button(labels[i])) apply_language(langs[i]);
if (active) ImGui::PopStyleColor();
}
}
section("editor");
{
ImVec2 avail = ImGui::GetContentRegionAvail();
float h = avail.y - 60.0f;
if (h < 220.0f) h = 220.0f;
fn::text_editor_render(s.ed, "##fn_text_editor_solo", ImVec2(-1, h));
if (fn::text_editor_is_dirty(s.ed)) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::warning);
ImGui::TextUnformatted("(modified)");
ImGui::PopStyleColor();
ImGui::SameLine();
if (ImGui::Button("clear dirty##te_solo")) fn::text_editor_clear_dirty(s.ed);
} else {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::TextUnformatted("(clean)");
ImGui::PopStyleColor();
}
}
code_block(
"auto* ed = fn::text_editor_create(fn::CodeLang::GLSL);\n"
"fn::text_editor_set_text(ed, src);\n"
"if (fn::text_editor_render(ed, \"##ed\", {600, 400}))\n"
" on_changed(fn::text_editor_get_text(ed));"
);
}
// ===========================================================================
// file_watcher — watcher cross-platform no bloqueante
// ===========================================================================
namespace {
constexpr const char* kWatchPath = "/tmp/fn_demo.glsl";
struct WatcherDemoState {
fn::FileWatcher* fw = nullptr;
bool active = false;
std::string err;
std::deque<std::string> events;
};
WatcherDemoState& watcher_state() {
static WatcherDemoState s;
return s;
}
void ensure_watcher() {
auto& s = watcher_state();
if (!s.fw) {
s.fw = fn::file_watcher_create();
s.active = fn::file_watcher_add(s.fw, kWatchPath);
if (!s.active) s.err = fn::file_watcher_last_error(s.fw);
}
}
const char* kind_label(fn::FileEvent::Kind k) {
switch (k) {
case fn::FileEvent::Modified: return "MODIFIED";
case fn::FileEvent::Created: return "CREATED";
case fn::FileEvent::Deleted: return "DELETED";
}
return "?";
}
void poll_and_log() {
auto& s = watcher_state();
if (!s.fw) return;
auto evs = fn::file_watcher_poll(s.fw);
for (auto& e : evs) {
char buf[512];
std::snprintf(buf, sizeof(buf), "[%s] %s", kind_label(e.kind), e.path.c_str());
s.events.push_back(buf);
}
while (s.events.size() > 200) s.events.pop_front();
}
bool touch_demo_file(std::string& err_out) {
FILE* f = std::fopen(kWatchPath, "a");
if (!f) { err_out = std::strerror(errno); return false; }
std::fprintf(f, "// touch %ld\n", (long)std::time(nullptr));
std::fclose(f);
return true;
}
} // namespace
void demo_file_watcher() {
using namespace fn_tokens;
demo_header("file_watcher", "v1.0.0",
"Watcher de archivos cross-platform no bloqueante. Linux: inotify. Windows: "
"ReadDirectoryChangesW. API: create / add / poll (drain) / destroy. Cap del "
"buffer de eventos: 200.");
ensure_watcher();
poll_and_log();
auto& s = watcher_state();
section("watcher state");
ImGui::Text("path: %s", kWatchPath);
ImGui::Text("active: %s", s.active ? "yes" : "no");
if (!s.err.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::error);
ImGui::TextWrapped("err: %s", s.err.c_str());
ImGui::PopStyleColor();
}
section("trigger events");
{
if (ImGui::Button("touch (append timestamp)")) {
std::string e;
if (!touch_demo_file(e)) s.err = "touch failed: " + e;
else s.err.clear();
// Si el archivo no existia al inicio, reintenta el add.
if (!s.active) {
s.active = fn::file_watcher_add(s.fw, kWatchPath);
if (!s.active) s.err = fn::file_watcher_last_error(s.fw);
}
}
ImGui::SameLine();
if (ImGui::Button("clear events")) s.events.clear();
ImGui::SameLine();
ImGui::TextDisabled("(o desde otro terminal: echo hi >> %s)", kWatchPath);
}
section("event log");
ImGui::Text("captured: %d", (int)s.events.size());
ImGui::BeginChild("##fw_evlog", ImVec2(0, 0), ImGuiChildFlags_Borders);
if (s.events.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::TextWrapped("Sin eventos. Pulsa touch o modifica el path desde otro terminal.");
ImGui::PopStyleColor();
} else {
for (auto it = s.events.rbegin(); it != s.events.rend(); ++it) {
ImGui::TextUnformatted(it->c_str());
}
}
ImGui::EndChild();
code_block(
"auto* fw = fn::file_watcher_create();\n"
"fn::file_watcher_add(fw, \"/tmp/foo.glsl\");\n"
"for (auto& e : fn::file_watcher_poll(fw)) {\n"
" handle_event(e.path, e.kind);\n"
"}"
);
}
} // namespace gallery
-211
View File
@@ -1,211 +0,0 @@
#include "demos.h"
#include "demo.h"
#include "viz/bar_chart.h"
#include "viz/pie_chart.h"
#include "viz/line_plot.h"
#include "viz/scatter_plot.h"
#include "viz/histogram.h"
#include "viz/sparkline.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cmath>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// bar_chart
// ---------------------------------------------------------------------------
void demo_bar_chart() {
demo_header("bar_chart", "v1.2.0",
"Barras verticales con ejes pineados, tooltip al hover y auto-rotacion 45 grados "
"de labels cuando no caben horizontalmente.");
section("Labels que caben horizontalmente");
{
const char* langs[] = {"go", "py", "ts", "sh", "cpp"};
float values[] = {412.0f, 187.0f, 94.0f, 63.0f, 36.0f};
bar_chart("##bar_short", langs, values, 5, 0.67f, 200.0f);
}
section("Labels largos que obligan a rotar");
{
const char* domains[] = {
"core", "infrastructure", "finance", "datascience",
"cybersecurity", "notebook", "browser"
};
float values[] = {412, 187, 94, 63, 42, 38, 22};
bar_chart("##bar_long", domains, values, 7, 0.67f, 240.0f);
}
code_block(
"const char* labels[] = {\"go\",\"py\",\"ts\",\"sh\",\"cpp\"};\n"
"float values[] = {412,187,94,63,36};\n"
"bar_chart(\"##lang\", labels, values, 5); // h=200 default\n"
"bar_chart(\"##lang\", labels, values, 5, 0.8f, 300); // bar_w + altura"
);
}
// ---------------------------------------------------------------------------
// pie_chart
// ---------------------------------------------------------------------------
void demo_pie_chart() {
demo_header("pie_chart", "v1.1.0",
"Pie/donut con aspect 1:1, ejes pineados y tooltip por slice con "
"valor absoluto + porcentaje.");
if (ImGui::BeginTable("##pie_grid", 2, ImGuiTableFlags_SizingStretchSame)) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
{
const char* labels[] = {"Pure", "Impure"};
float values[] = {412.0f, 278.0f};
variant_label("Pie (radius auto)");
pie_chart("##pie_auto", labels, values, 2, 0.0f, 260.0f);
}
ImGui::TableSetColumnIndex(1);
{
const char* labels[] = {"function", "pipeline", "component"};
float values[] = {618.0f, 42.0f, 230.0f};
variant_label("Donut (radius = -0.45)");
pie_chart("##pie_donut", labels, values, 3, -0.45f, 260.0f);
}
ImGui::EndTable();
}
code_block(
"const char* labels[] = {\"Pure\",\"Impure\"};\n"
"float values[] = {412, 278};\n"
"pie_chart(\"##p\", labels, values, 2); // pie auto\n"
"pie_chart(\"##p\", labels, values, 2, -0.45f, 260); // donut"
);
}
// ---------------------------------------------------------------------------
// line_plot
// ---------------------------------------------------------------------------
void demo_line_plot() {
demo_header("line_plot", "v1.1.0",
"Line plot 2D con limites de ejes calculados de min/max y pineados. "
"Sin auto-fit animado, sin pan/zoom.");
constexpr int N = 100;
static float xs[N], ys[N];
static bool init = false;
if (!init) {
for (int i = 0; i < N; i++) {
xs[i] = static_cast<float>(i) * 0.1f;
ys[i] = std::sin(xs[i]) + 0.3f * std::sin(xs[i] * 3.5f);
}
init = true;
}
line_plot("##line", xs, ys, N, 240.0f);
code_block(
"line_plot(\"##series\", xs, ys, count); // h=200 default\n"
"line_plot(\"##series\", xs, ys, count, 300.0f); // custom height"
);
}
// ---------------------------------------------------------------------------
// scatter_plot
// ---------------------------------------------------------------------------
void demo_scatter_plot() {
demo_header("scatter_plot", "v1.1.0",
"Puntos dispersos con ejes pineados (5% headroom). Sin interaccion.");
constexpr int N = 120;
static float xs[N], ys[N];
static bool init = false;
if (!init) {
unsigned seed = 1234;
auto rnd = [&]() {
seed = seed * 1103515245u + 12345u;
return static_cast<float>((seed >> 16) & 0x7fff) / 32768.0f;
};
for (int i = 0; i < N; i++) {
xs[i] = rnd() * 10.0f;
ys[i] = 0.5f * xs[i] + rnd() * 3.0f;
}
init = true;
}
scatter_plot("##sc", xs, ys, N, 240.0f);
code_block(
"scatter_plot(\"##xy\", xs, ys, count, 240.0f);"
);
}
// ---------------------------------------------------------------------------
// histogram
// ---------------------------------------------------------------------------
void demo_histogram() {
demo_header("histogram", "v1.1.0",
"Histograma con bins automaticos (Sturges) o manuales. Usa AutoFit "
"para los bins + Lock para bloquear pan/zoom.");
constexpr int N = 300;
static float vals[N];
static bool init = false;
if (!init) {
unsigned seed = 42;
auto rnd = [&]() {
seed = seed * 1103515245u + 12345u;
return static_cast<float>((seed >> 16) & 0x7fff) / 32768.0f;
};
// Aproximacion de distribucion normal via box-muller simplificado
for (int i = 0; i < N; i++) {
float u1 = rnd() + 1e-6f;
float u2 = rnd();
vals[i] = std::sqrt(-2.0f * std::log(u1))
* std::cos(2.0f * 3.14159f * u2);
}
init = true;
}
histogram("##hist", vals, N, -1, 240.0f);
code_block(
"histogram(\"##h\", values, count); // bins=Sturges\n"
"histogram(\"##h\", values, count, 30, 300.0f); // 30 bins, h=300"
);
}
// ---------------------------------------------------------------------------
// sparkline
// ---------------------------------------------------------------------------
void demo_sparkline() {
demo_header("sparkline", "v1.0.0",
"Mini grafico de lineas inline (rellenado con alpha + linea). "
"Pensado para tablas, KPI cards, headers.");
float up[] = {10, 12, 11, 15, 18, 17, 20};
float down[] = {30, 28, 29, 25, 22, 24, 20};
float flat[] = {10, 10, 10, 10, 10, 10, 10};
ImGui::Text("Trending up "); ImGui::SameLine();
sparkline("##up", up, 7, ImVec4(0.35f, 0.85f, 0.45f, 1.0f), 140.0f, 22.0f);
ImGui::Text("Trending down"); ImGui::SameLine();
sparkline("##down", down, 7, ImVec4(0.90f, 0.30f, 0.30f, 1.0f), 140.0f, 22.0f);
ImGui::Text("Flat "); ImGui::SameLine();
sparkline("##flat", flat, 7, ImVec4(0.55f, 0.55f, 0.55f, 1.0f), 140.0f, 22.0f);
code_block(
"float history[] = {10,12,11,15,18,17,20};\n"
"sparkline(\"##rev\", history, 7, /*color=*/{0.35,0.85,0.45,1}, 140, 22);"
);
}
} // namespace gallery
-230
View File
@@ -1,230 +0,0 @@
// primitives_gallery — catalogo visual interactivo de los primitivos UI
// del registry (cpp/functions/core y cpp/functions/viz).
//
// Sidebar izquierdo con lista de primitivos agrupados por dominio; panel
// derecho renderiza la demo del item seleccionado (+ snippet de codigo).
//
// Rol: smoke test visual + documentacion viva + build gate en CI.
// NO se conecta a sqlite_api ni a ningun backend. Datos sinteticos.
#include "app_base.h"
#include "imgui.h"
#include "core/fullscreen_window.h"
#include "core/tokens.h"
#include "core/page_header.h"
#include "core/toast.h"
#include "core/app_menubar.h"
#include "core/tree_view.h"
#include "demos.h"
#include "demo.h"
#include "capture.h"
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <sys/stat.h>
#include <vector>
struct DemoEntry {
const char* id; // id estable, apto para comparar seleccion
const char* label; // texto en sidebar
const char* category; // "Core" o "Viz"
void (*fn)(); // puntero a la demo_xxx
};
static const DemoEntry k_demos[] = {
// Core
{"button", "button", "Core", &gallery::demo_button},
{"icon_button", "icon_button", "Core", &gallery::demo_icon_button},
{"toolbar", "toolbar", "Core", &gallery::demo_toolbar},
{"modal_dialog", "modal_dialog", "Core", &gallery::demo_modal},
{"text_input", "text_input", "Core", &gallery::demo_text_input},
{"select", "select", "Core", &gallery::demo_select},
{"toast", "toast + inbox", "Core", &gallery::demo_toast},
{"tree_view", "tree_view", "Core", &gallery::demo_tree_view},
{"badge", "badge", "Core", &gallery::demo_badge},
{"empty_state", "empty_state", "Core", &gallery::demo_empty_state},
{"page_header", "page_header", "Core", &gallery::demo_page_header},
{"dashboard_panel", "dashboard_panel", "Core", &gallery::demo_dashboard_panel},
{"kpi_card", "kpi_card", "Core", &gallery::demo_kpi_card},
{"text_editor", "text_editor", "Core", &gallery::demo_text_editor}, // wave 1
{"file_watcher", "file_watcher", "Core", &gallery::demo_file_watcher}, // wave 1
{"process_runner", "process_runner", "Core", &gallery::demo_process_runner},
{"tween", "tween_curves", "Core", &gallery::demo_tween},
{"bezier_editor", "bezier_editor", "Core", &gallery::demo_bezier_editor},
{"timeline", "timeline", "Core", &gallery::demo_timeline},
{"sql_workbench", "sql_workbench", "Core", &gallery::demo_sql_workbench}, // issue 0032
// Viz
{"bar_chart", "bar_chart", "Viz", &gallery::demo_bar_chart},
{"pie_chart", "pie_chart", "Viz", &gallery::demo_pie_chart},
{"line_plot", "line_plot", "Viz", &gallery::demo_line_plot},
{"scatter_plot", "scatter_plot", "Viz", &gallery::demo_scatter_plot},
{"histogram", "histogram", "Viz", &gallery::demo_histogram},
{"sparkline", "sparkline", "Viz", &gallery::demo_sparkline},
{"graph_viewport", "graph_viewport", "Viz", &gallery::demo_graph},
{"graph_styles", "graph_styles", "Viz", &gallery::demo_graph_styles}, // issue 0049f
{"candlestick", "candlestick", "Viz", &gallery::demo_candlestick},
{"gauge", "gauge", "Viz", &gallery::demo_gauge},
{"heatmap", "heatmap", "Viz", &gallery::demo_heatmap},
{"table_view", "table_view", "Viz", &gallery::demo_table_view},
{"surface_plot_3d", "surface_plot_3d", "Viz", &gallery::demo_surface_plot_3d},
{"scatter_3d", "scatter_3d", "Viz", &gallery::demo_scatter_3d},
{"mesh_viewer", "mesh_viewer", "Viz", &gallery::demo_mesh_viewer},
{"treemap", "treemap", "Viz", &gallery::demo_treemap},
{"sankey", "sankey", "Viz", &gallery::demo_sankey},
{"chord", "chord", "Viz", &gallery::demo_chord},
{"contour", "contour", "Viz", &gallery::demo_contour},
{"voronoi", "voronoi", "Viz", &gallery::demo_voronoi},
// Gfx (shaders_lab core)
{"shader_canvas", "shader_canvas", "Gfx", &gallery::demo_shader_canvas},
{"gl_texture", "gl_texture_load", "Gfx", &gallery::demo_gl_texture}, // wave 1
{"gl_info", "gl_info", "Gfx", &gallery::demo_gl_info}, // issue 0049b
};
static constexpr int k_demo_count = sizeof(k_demos) / sizeof(k_demos[0]);
static std::string g_selected_id = "button";
static const DemoEntry* find_demo(const std::string& id) {
for (int i = 0; i < k_demo_count; i++) {
if (id == k_demos[i].id) return &k_demos[i];
}
return &k_demos[0];
}
static void draw_sidebar() {
ImGui::BeginChild("##gallery_sidebar", ImVec2(220, 0),
ImGuiChildFlags_Borders);
// Agrupar por categoria como rama del tree_view (categorias abiertas por
// defecto). Cada demo es una hoja seleccionable.
int i = 0;
while (i < k_demo_count) {
const char* category = k_demos[i].category;
// Default-open la rama la primera vez que se abre el sidebar.
ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
if (fn_ui::tree_branch_begin(category, category, /*selected=*/false)) {
// Recorrer todas las demos consecutivas con esta misma categoria.
while (i < k_demo_count
&& std::strcmp(k_demos[i].category, category) == 0) {
const auto& d = k_demos[i];
const bool selected = (g_selected_id == d.id);
fn_ui::tree_leaf(d.id, d.label, selected);
if (fn_ui::tree_node_clicked()) {
g_selected_id = d.id;
}
i++;
}
fn_ui::tree_branch_end();
} else {
// Rama colapsada — saltar todos sus items.
while (i < k_demo_count
&& std::strcmp(k_demos[i].category, category) == 0) {
i++;
}
}
}
ImGui::EndChild();
}
static void render() {
// Theme y gl_loader gestionados por fn::run_app (theme=FnDark por defecto,
// init_gl_loader=true en AppConfig). Menubar via run_app.
// auto_dockspace=false porque usamos fullscreen_window que ocupa todo.
fullscreen_window_begin("##gallery");
page_header_begin("Primitives Gallery",
"Visual catalog of fn_registry C++ UI primitives");
page_header_end();
if (ImGui::BeginTable("##layout", 2,
ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingFixedFit)) {
ImGui::TableSetupColumn("sidebar", ImGuiTableColumnFlags_WidthFixed, 220.0f);
ImGui::TableSetupColumn("content", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
draw_sidebar();
ImGui::TableSetColumnIndex(1);
ImGui::BeginChild("##gallery_content", ImVec2(0, 0),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_HorizontalScrollbar);
// Cuando cambia el tamaño de fuente (Settings > Size), el contenido
// del child crece/encoge pero la posicion de scroll en pixeles
// no — efecto: lo visible "se baja". Escalamos scroll_y por el
// ratio de fuentes para mantener la misma linea logica arriba.
{
static float s_prev_font_size = 0.0f;
float cur_font_size = ImGui::GetStyle().FontSizeBase;
if (s_prev_font_size > 0.0f &&
std::fabs(s_prev_font_size - cur_font_size) > 0.01f) {
ImGui::SetScrollY(ImGui::GetScrollY() *
(cur_font_size / s_prev_font_size));
}
s_prev_font_size = cur_font_size;
}
const DemoEntry* d = find_demo(g_selected_id);
if (d && d->fn) d->fn();
ImGui::EndChild();
ImGui::EndTable();
}
fullscreen_window_end();
// Toasts se renderizan encima para que el demo de toast funcione aqui tambien.
fn_ui::toast_render();
}
int main(int argc, char** argv) {
// Capture mode: `primitives_gallery --capture <output_dir>` corre cada
// demo en una ventana GLFW invisible y guarda PNG por demo. Para CI/golden.
for (int i = 1; i < argc; i++) {
if (std::strcmp(argv[i], "--capture") == 0) {
if (i + 1 >= argc) {
std::fprintf(stderr, "--capture requires an output dir argument\n");
return 2;
}
const char* out_dir = argv[i + 1];
// Best-effort mkdir (idempotente). Windows mkdir() solo acepta el path.
#if defined(_WIN32)
mkdir(out_dir);
#else
mkdir(out_dir, 0755);
#endif
std::vector<gallery::CaptureItem> items;
items.reserve(k_demo_count);
for (int j = 0; j < k_demo_count; j++) {
items.push_back({k_demos[j].id, k_demos[j].fn});
}
gallery::CaptureConfig cfg;
cfg.output_dir = out_dir;
cfg.warmup_frames = 3;
cfg.capture_w = 800;
cfg.capture_h = 600;
const bool ok = gallery::run_capture(cfg, items);
return ok ? 0 : 1;
}
}
return fn::run_app(
{.title = "fn_registry · Primitives Gallery",
.width = 1400,
.height = 900,
.viewports = true,
.about = {.name = "Primitives Gallery",
.version = "0.4.0",
.description = "Visual catalog of fn_registry C++ UI primitives. Now on OpenGL 4.3 core (compute, SSBOs, image load/store) — ver demo gl_info."},
.init_gl_loader = true,
.auto_dockspace = false,
.log = {"primitives_gallery.log", 1}},
render
);
}
@@ -1,47 +0,0 @@
# Tables playground (cpp_apps.md / playgrounds.md). NO se indexa.
# Build flag FN_TQL_DUCKDB=ON activa el adapter tql_duckdb (issue 0080).
option(FN_TQL_DUCKDB "Enable DuckDB SQL execution adapter for tables playground" OFF)
set(_TABLES_SRC
main.cpp
data_table.cpp
data_table_logic.cpp
llm_anthropic.cpp
lua_engine.cpp
tql.cpp
tql_to_sql.cpp
viz.cpp
)
set(_TABLES_TEST_SRC
self_test.cpp
data_table_logic.cpp
llm_anthropic.cpp
lua_engine.cpp
tql.cpp
tql_to_sql.cpp
)
if(FN_TQL_DUCKDB)
list(APPEND _TABLES_SRC tql_duckdb.cpp)
list(APPEND _TABLES_TEST_SRC tql_duckdb.cpp)
endif()
add_imgui_app(tables_playground ${_TABLES_SRC})
target_link_libraries(tables_playground PRIVATE lua54 implot)
if(FN_TQL_DUCKDB)
target_compile_definitions(tables_playground PRIVATE FN_TQL_DUCKDB=1)
target_link_libraries(tables_playground PRIVATE duckdb_vendored)
duckdb_copy_runtime(tables_playground)
endif()
# Self-test E2E (logica pura + lua_engine + tql).
add_executable(tables_playground_self_test ${_TABLES_TEST_SRC})
target_include_directories(tables_playground_self_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/functions
)
target_link_libraries(tables_playground_self_test PRIVATE lua54)
if(FN_TQL_DUCKDB)
target_compile_definitions(tables_playground_self_test PRIVATE FN_TQL_DUCKDB=1)
target_link_libraries(tables_playground_self_test PRIVATE duckdb_vendored)
duckdb_copy_runtime(tables_playground_self_test)
endif()
File diff suppressed because it is too large Load Diff
@@ -1,18 +0,0 @@
#pragma once
#include "data_table_logic.h"
namespace data_table {
// Render barra-de-chips + tabla. Mutates `st` en respuesta a interaccion.
// `declared_types` opcional: array paralelo a headers con ColumnType por col.
// Si nullptr o ColumnType::Auto -> resuelve via auto_detect_type.
// API unificada: `tables` lista todas las tablas disponibles. La que actua como
// main la elige State.main_source (vacio -> tables[0]). El resto se exponen
// como joinables en la UI cuando size > 1.
void render(const char* id,
const std::vector<TableInput>& tables,
State& st,
bool show_chrome = true);
} // namespace data_table
File diff suppressed because it is too large Load Diff
@@ -1,206 +0,0 @@
// Logica pura del playground data_table. Sin ImGui — testable headless.
// TIPOS promovidos al registry (issue 0081). Este header solo declara
// funciones; los types vienen de cpp/functions/core/data_table_types.h.
#pragma once
#include "core/data_table_types.h"
#include <string>
#include <utility>
#include <vector>
namespace data_table {
// ----------------------------------------------------------------------------
// Helpers para Op y ColumnType.
// ----------------------------------------------------------------------------
const char* op_label(Op o);
bool op_is_string_only(Op o);
const char* column_type_name(ColumnType t);
const char* column_type_icon(ColumnType t); // UTF-8 Tabler icon
// Ops permitidos para cada tipo. Devuelve vector ordenado.
std::vector<Op> ops_for_type(ColumnType t);
// Auto-detect via sample: escanea hasta `sample_n` celdas no-vacias.
ColumnType auto_detect_type(const char* const* cells, int rows, int cols,
int col, int sample_n = 64);
// Tipo efectivo: si declared != Auto -> declared; else auto_detect.
ColumnType effective_type(ColumnType declared,
const char* const* cells, int rows, int cols, int col);
// ----------------------------------------------------------------------------
// Aggregation helpers.
// ----------------------------------------------------------------------------
const char* agg_fn_name(AggFn f);
// Pure: alias por defecto cuando agg.alias esta vacio.
// count -> "count"
// distinct col -> "distinct_<col>"
// percentile p -> "p<arg*100>_<col>" (ej. p95_size_kb)
// resto -> "<fn>_<col>" (ej. avg_size_kb)
std::string aggregation_alias(const Aggregation& a);
// Pure: tipo del output de la aggregation.
ColumnType aggregation_type(const Aggregation& a,
const std::vector<std::string>& in_headers,
const std::vector<ColumnType>& in_types);
// ----------------------------------------------------------------------------
// Compute pipeline.
// ----------------------------------------------------------------------------
// Pure: ejecuta un Stage sobre los cells de entrada. Aplica filter -> (group+agg|passthrough) -> sort.
StageOutput compute_stage(const char* const* in_cells, int in_rows, int in_cols,
const std::vector<std::string>& in_headers,
const std::vector<ColumnType>& in_types,
const Stage& stage);
// Pure: aplica filtros usando headers para resolver f.col.
std::vector<int> apply_filters(const char* const* cells, int rows, int cols,
const std::vector<Filter>& filters);
// Pure: helper para drill-down. Devuelve un Filter Op::Eq sobre col_idx con
// el value indicado.
Filter make_drill_filter(int col_idx, const std::string& value);
// ----------------------------------------------------------------------------
// ViewMode helpers.
// ----------------------------------------------------------------------------
const char* view_mode_token(ViewMode m);
const char* view_mode_label(ViewMode m);
ViewMode view_mode_from_token(const char* s);
int view_mode_min_cols(ViewMode m);
bool view_mode_needs_numeric(ViewMode m);
bool view_mode_needs_category(ViewMode m);
bool view_mode_needs_aggregation(ViewMode m);
// Lista completa de modos para el selector UI.
const ViewMode* all_view_modes(int* n_out);
// ----------------------------------------------------------------------------
// Joins (MBQL-style). Ver issue 0078.
// ----------------------------------------------------------------------------
const char* join_strategy_token(JoinStrategy s);
JoinStrategy join_strategy_from_token(const char* s);
const char* join_strategy_label(JoinStrategy s);
// Pure: resuelve indice del main entre `tables` segun `main_source`.
int resolve_main_idx(const std::vector<TableInput>& tables, const std::string& main_source);
// Pure: aplica un join sobre dos tablas.
StageOutput join_tables(const char* const* left_cells, int left_rows, int left_cols,
const std::vector<std::string>& left_headers,
const std::vector<ColumnType>& left_types,
const TableInput& right,
const Join& jn);
// ----------------------------------------------------------------------------
// Drill apply/undo (fase 10).
// ----------------------------------------------------------------------------
bool apply_drill_step(State& st, const DrillStep& step);
bool undo_drill_step(State& st, const DrillStep& step);
// Pure (fase 10): drill-up. Decrementa active_stage si > 0.
bool drill_up(State& st);
// Pure (fase 10): serializa una fila a TSV.
std::string row_to_tsv(const char* const* cells, int rows, int cols,
int row_idx, const std::vector<std::string>& headers);
// Pure (fase 10): construye filters Op::Eq desde una fila.
std::vector<Filter> build_filters_from_row(const char* const* cells, int rows,
int cols, int row_idx);
// ----------------------------------------------------------------------------
// Date granularity helpers (fase 10).
// ----------------------------------------------------------------------------
const char* date_granularity_token(DateGranularity g);
DateGranularity date_granularity_from_token(const char* s);
DateGranularity parse_breakout_granularity(const std::string& breakout,
std::string& col_out);
std::string compose_breakout(const std::string& col, DateGranularity g);
void column_min_max(const char* const* cells, int rows, int cols, int col_idx,
std::string& min_out, std::string& max_out);
// Hit-tests para click-to-drill sobre charts (fase 10).
int nearest_index_1d(double target, const double* xs, int n);
int nearest_index_2d(double tx, double ty,
const double* xs, const double* ys, int n);
double pie_angle(double cx, double cy, double mx, double my);
int pie_slice_at_angle(double angle, const double* sums, int n);
void heatmap_cell_at(double px, double py, int rows, int cols,
int& row_out, int& col_out);
// Date trunc + auto + presets.
std::string truncate_date(const std::string& date, DateGranularity g);
DateGranularity auto_date_granularity(const std::string& min_ymd,
const std::string& max_ymd);
const char* filter_preset_label(FilterPreset p);
std::vector<Filter> build_preset_filters(FilterPreset preset, int col,
const std::string& today_ymd);
// ----------------------------------------------------------------------------
// Misc helpers.
// ----------------------------------------------------------------------------
bool parse_number(const char* s, double& out);
bool compare(const char* a, const char* b, Op op);
std::vector<int> compute_visible_rows(const char* const* cells,
int rows, int cols,
const State& st);
void reorder_column(State& st, int src, int dst);
int find_open_bracket(const char* buf, int len, int cursor, std::string& filter_text);
std::string insert_column_ref(const std::string& src, int start, int cursor,
const std::string& name, int& new_cursor);
std::string csv_escape(const char* s);
std::string build_tsv(const char* const* cells, int rows, int cols,
const char* const* headers,
const std::vector<int>& col_order,
const std::vector<bool>& col_visible,
const std::vector<int>& visible_rows,
int view_row_lo, int view_row_hi,
int view_col_lo, int view_col_hi);
std::string build_csv(const char* const* cells, int rows, int cols,
const char* const* headers,
const std::vector<int>& col_order,
const std::vector<bool>& col_visible,
const std::vector<int>& visible_rows);
// ----------------------------------------------------------------------------
// Column statistics (no movido todavia al registry).
// ----------------------------------------------------------------------------
struct ColStats {
int total = 0;
int empty_count = 0;
int unique_count = 0;
bool unique_capped = false;
bool numeric = false;
int numeric_count = 0;
double min = 0;
double max = 0;
double sum = 0;
double mean = 0;
double p25 = 0;
double p50 = 0;
double p75 = 0;
std::vector<float> hist;
std::vector<std::pair<std::string,int>> top_categories;
};
constexpr int HIST_BINS = 24;
ColStats compute_column_stats(const char* const* cells, int rows, int cols,
int col, int unique_cap = 100000,
const int* indices = nullptr, int n_indices = 0);
} // namespace data_table
@@ -1,29 +0,0 @@
#!/usr/bin/env bash
# E2E playground tables. Compila + corre self-test linux + windows (si
# mingw esta disponible). Sale 0 si todo pasa.
set -euo pipefail
ROOT="${FN_REGISTRY_ROOT:-$(cd "$(dirname "$0")/../../../../.." && pwd)}"
cd "$ROOT"
echo "[e2e] linux build + self_test"
cmake -B cpp/build -S cpp >/dev/null
cmake --build cpp/build --target tables_playground_self_test -j"$(nproc)" >/dev/null
./cpp/build/apps/primitives_gallery/playground/tables/tables_playground_self_test
if command -v x86_64-w64-mingw32-g++ >/dev/null 2>&1; then
echo "[e2e] windows cross-build (mingw)"
source "$ROOT/bash/functions/infra/build_cpp_windows.sh"
build_cpp_windows tables_playground_self_test >/dev/null
echo "[e2e] windows self_test via wine si disponible"
EXE="$ROOT/cpp/build/windows/apps/primitives_gallery/playground/tables/tables_playground_self_test.exe"
if [ -f "$EXE" ]; then
if command -v wine >/dev/null 2>&1; then
wine "$EXE" || { echo "[e2e] FAIL windows self_test (wine)"; exit 1; }
else
echo "[e2e] SKIP wine no instalado; binario en $EXE"
fi
fi
fi
echo "[e2e] OK"
@@ -1,295 +0,0 @@
// llm_anthropic.cpp — cliente Anthropic minimal via cURL popen.
// Ver issue 0080.
#include "llm_anthropic.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sstream>
#include <string>
namespace llm_anthropic {
using namespace data_table;
namespace {
// JSON escape minimal.
std::string json_escape(const std::string& s) {
std::string o;
o.reserve(s.size() + 8);
for (char c : s) {
switch (c) {
case '"': o += "\\\""; break;
case '\\': o += "\\\\"; break;
case '\n': o += "\\n"; break;
case '\r': o += "\\r"; break;
case '\t': o += "\\t"; break;
case '\b': o += "\\b"; break;
case '\f': o += "\\f"; break;
default:
if ((unsigned char)c < 0x20) {
char buf[8];
std::snprintf(buf, sizeof(buf), "\\u%04x", (int)(unsigned char)c);
o += buf;
} else {
o += c;
}
}
}
return o;
}
const char* col_type_doc(ColumnType t) {
switch (t) {
case ColumnType::String: return "string";
case ColumnType::Int: return "int";
case ColumnType::Float: return "float";
case ColumnType::Bool: return "bool";
case ColumnType::Date: return "date";
case ColumnType::Json: return "json";
case ColumnType::Auto: return "auto";
}
return "?";
}
std::string build_schema_block(const AskInput& in) {
std::ostringstream os;
os << "Available columns (stage 0 input):\n";
for (size_t i = 0; i < in.col_names.size(); ++i) {
os << " - " << in.col_names[i] << ": "
<< col_type_doc(i < in.col_types.size() ? in.col_types[i] : ColumnType::String)
<< "\n";
}
if (!in.joinable_names.empty()) {
os << "Joinable tables (for join clause):\n";
for (const auto& n : in.joinable_names) os << " - " << n << "\n";
}
return os.str();
}
std::string build_system_prompt(OutputMode mode) {
if (mode == OutputMode::TQL) {
return
"You are a TQL (Table Query Language) expert. Output ONLY a Lua code block. "
"TQL is a Lua table with shape:\n"
" return { version=1, display=\"table\"|\"bar\"|\"line\"|...,\n"
" main_source=\"name\", joins={ {alias,source,on,strategy,fields},... },\n"
" stages={ {filter={{op,col,value},...}, breakout={...}, aggregation={...}, sort={...} },... },\n"
" columns={ name = {type=\"int|float|...\", formula=\"[col]+1\"},... }\n"
" }\n"
"Stage 0 = Raw (filters + derived + sort, NO breakouts/aggs).\n"
"Stage 1+ groups (breakouts + aggregations).\n"
"Breakout granularity: append :year|:month|:week|:day|:hour to col name.\n"
"Aggregation functions: count|sum|avg|min|max|distinct|stddev|median|p25|p75|p90|p99|percentile.\n"
"Filter ops: '='|'!='|'<'|'<='|'>'|'>='|'contains'|'!contains'|'starts'|'ends'.\n"
"Sort: {{dir, col}, ...} where dir = 'asc'|'desc'.\n"
"Join strategies: 'left'|'inner'|'right'|'full'.\n"
"Formulas use Lua expression syntax with [col] for column refs.\n"
"Output format: ```lua\\n...\\n```";
}
return
"You are a DuckDB SQL expert. Output ONLY a SQL code block compatible with DuckDB.\n"
"Use CTEs to chain stages. Use date_trunc('month', col) for granularity.\n"
"Use quantile_cont(col, p) for percentiles. Use ? for bound params.\n"
"Joins: LEFT/INNER/RIGHT/FULL OUTER JOIN. String concat: ||. Aggregations: standard SQL.\n"
"Output format: ```sql\\n...\\n```";
}
} // anon
std::string build_request_body(const AskInput& in) {
std::string system_msg = build_system_prompt(in.mode);
std::string schema = build_schema_block(in);
std::ostringstream user_msg;
user_msg << "Question: " << in.question << "\n\n"
<< schema << "\n";
if (!in.tql_current.empty()) {
user_msg << "Current TQL:\n```lua\n" << in.tql_current << "\n```\n";
}
std::string model = in.model.empty() ? "claude-sonnet-4-6" : in.model;
std::ostringstream body;
body << "{"
<< "\"model\":\"" << json_escape(model) << "\","
<< "\"max_tokens\":" << in.max_tokens << ","
<< "\"system\":\"" << json_escape(system_msg) << "\","
<< "\"messages\":[{"
<< "\"role\":\"user\","
<< "\"content\":\"" << json_escape(user_msg.str()) << "\""
<< "}]"
<< "}";
return body.str();
}
std::string extract_code_block(const std::string& raw, const std::string& lang) {
// Buscar ```<lang> primero, sino ``` plain.
std::string fence_lang = "```" + lang;
auto pos = raw.find(fence_lang);
size_t code_start = std::string::npos;
if (pos != std::string::npos) {
code_start = pos + fence_lang.size();
} else {
pos = raw.find("```");
if (pos != std::string::npos) {
code_start = pos + 3;
// skip optional lang tag
while (code_start < raw.size() && raw[code_start] != '\n' &&
raw[code_start] != '\r' && std::isalnum((unsigned char)raw[code_start])) {
++code_start;
}
}
}
if (code_start == std::string::npos) {
// No fence — return raw stripped.
size_t i = 0; while (i < raw.size() && std::isspace((unsigned char)raw[i])) ++i;
size_t j = raw.size(); while (j > i && std::isspace((unsigned char)raw[j-1])) --j;
return raw.substr(i, j - i);
}
// Skip newline tras fence.
if (code_start < raw.size() && raw[code_start] == '\n') ++code_start;
auto end = raw.find("```", code_start);
if (end == std::string::npos) end = raw.size();
std::string code = raw.substr(code_start, end - code_start);
// Trim trailing newline.
while (!code.empty() && (code.back() == '\n' || code.back() == '\r')) code.pop_back();
return code;
}
std::string parse_response_text(const std::string& json) {
// Buscar pattern: "text":"..."
// Simple: primer occurrence de \"text\":\" tras \"type\":\"text\"
auto t = json.find("\"text\"");
while (t != std::string::npos) {
// Skip "text"
size_t i = t + 6;
// Skip whitespace y :
while (i < json.size() && (json[i] == ' ' || json[i] == ':' || json[i] == '\t')) ++i;
if (i >= json.size() || json[i] != '"') {
t = json.find("\"text\"", t + 1);
continue;
}
++i;
std::string out;
while (i < json.size() && json[i] != '"') {
if (json[i] == '\\' && i + 1 < json.size()) {
char esc = json[i+1];
if (esc == 'n') out += '\n';
else if (esc == 't') out += '\t';
else if (esc == 'r') out += '\r';
else if (esc == '"') out += '"';
else if (esc == '\\') out += '\\';
else if (esc == '/') out += '/';
else if (esc == 'u' && i + 5 < json.size()) {
// basic ascii \uXXXX
int code = 0;
for (int k = 0; k < 4; ++k) {
char c = json[i + 2 + k];
int v = (c >= '0' && c <= '9') ? c - '0'
: (c >= 'a' && c <= 'f') ? c - 'a' + 10
: (c >= 'A' && c <= 'F') ? c - 'A' + 10 : 0;
code = code * 16 + v;
}
if (code < 128) out += (char)code;
else out += '?';
i += 5;
} else {
out += esc;
}
i += 2;
} else {
out += json[i++];
}
}
return out;
}
return "";
}
namespace {
// Lee API key segun prioridad: param > env FN_LLM_API_KEY > pass anthropic/api-key.
std::string resolve_api_key(const std::string& provided) {
if (!provided.empty()) return provided;
const char* env = std::getenv("FN_LLM_API_KEY");
if (env && *env) return env;
// pass anthropic/api-key | head -n1
FILE* p = popen("pass anthropic/api-key 2>/dev/null | head -n1", "r");
if (!p) return "";
std::string out;
char buf[256];
while (fgets(buf, sizeof(buf), p)) out += buf;
pclose(p);
while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back();
return out;
}
} // anon
std::string call_api(const std::string& body, const std::string& api_key,
std::string& error_out) {
error_out.clear();
// Test injection
const char* mock = std::getenv("FN_LLM_MOCK_RESPONSE");
if (mock && *mock) return mock;
std::string key = resolve_api_key(api_key);
if (key.empty()) {
error_out = "no API key (set FN_LLM_API_KEY env, pass param, or `pass anthropic/api-key`)";
return "";
}
const char* endpoint_env = std::getenv("FN_LLM_ENDPOINT");
std::string endpoint = endpoint_env && *endpoint_env
? endpoint_env
: "https://api.anthropic.com/v1/messages";
// popen "w+" no portable. Write body a tmp file y leer respuesta de curl
// por redireccion. Portable Unix/Mingw.
std::string tmp_in = std::tmpnam(nullptr);
std::string tmp_out = std::tmpnam(nullptr);
{
FILE* f = std::fopen(tmp_in.c_str(), "w");
if (!f) { error_out = "tmp file write fail"; return ""; }
std::fwrite(body.data(), 1, body.size(), f);
std::fclose(f);
}
std::string cmd2 = "curl -sS -X POST "
"-H \"content-type: application/json\" "
"-H \"anthropic-version: 2023-06-01\" "
"-H \"x-api-key: " + key + "\" "
"--data-binary @" + tmp_in + " " + endpoint
+ " > " + tmp_out + " 2>&1";
int rc = std::system(cmd2.c_str());
std::string resp;
{
FILE* f = std::fopen(tmp_out.c_str(), "r");
if (f) {
char buf[4096];
size_t n;
while ((n = std::fread(buf, 1, sizeof(buf), f)) > 0) resp.append(buf, n);
std::fclose(f);
}
}
std::remove(tmp_in.c_str());
std::remove(tmp_out.c_str());
if (rc != 0) {
error_out = "curl exit " + std::to_string(rc) + ": " + resp;
return "";
}
return resp;
}
AskResult ask(const AskInput& in, const std::string& api_key) {
AskResult r;
std::string body = build_request_body(in);
std::string raw_json = call_api(body, api_key, r.error);
if (!r.error.empty()) return r;
r.raw = parse_response_text(raw_json);
std::string lang = (in.mode == OutputMode::TQL) ? "lua" : "sql";
r.code = extract_code_block(r.raw, lang);
return r;
}
} // namespace llm_anthropic
@@ -1,58 +0,0 @@
// llm_anthropic: cliente HTTP minimal a Anthropic Claude API.
// Sin deps externas (cURL via popen).
// Ver issue 0080.
#pragma once
#include "data_table_logic.h"
#include "tql_to_sql.h"
#include <string>
#include <vector>
namespace llm_anthropic {
enum class OutputMode { TQL, SQL };
struct AskInput {
std::string question; // pregunta NL
std::string tql_current; // TQL actual (emitido)
std::vector<std::string> col_names; // schema input
std::vector<data_table::ColumnType> col_types;
std::vector<std::string> joinable_names; // tables disponibles para join
OutputMode mode = OutputMode::TQL;
std::string model; // empty -> default
int max_tokens = 8192;
};
struct AskResult {
std::string code; // bloque ```lua o ```sql extraido (sin fences)
std::string raw; // texto completo de la respuesta
std::string error; // non-empty si fallo
int tokens_in = 0;
int tokens_out = 0;
};
// Pure: construye el system prompt y user message JSON-escapado.
// Devuelve el JSON body completo POST al endpoint /v1/messages.
std::string build_request_body(const AskInput& in);
// Pure: extrae primer ```<lang>\n ... \n``` bloque de `raw`. lang = "lua"|"sql".
// Si no encuentra fence, retorna raw stripped.
std::string extract_code_block(const std::string& raw, const std::string& lang);
// Pure: extrae texto del JSON de respuesta Anthropic.
// Busca `"content":[{"type":"text","text":"..."}]` y devuelve el text.
std::string parse_response_text(const std::string& json_body);
// Impure: lanza cURL via popen, posts `body` al endpoint Anthropic /v1/messages,
// retorna response body (JSON crudo). API key leida de:
// 1. parametro `api_key` si non-empty
// 2. env FN_LLM_API_KEY
// 3. `pass anthropic/api-key | head -n1`
// Si FN_LLM_MOCK_RESPONSE env set, retorna su valor (test injection).
std::string call_api(const std::string& body, const std::string& api_key,
std::string& error_out);
// Orchestrator: build prompt + POST + parse. Convenience wrapper.
AskResult ask(const AskInput& in, const std::string& api_key = "");
} // namespace llm_anthropic
@@ -1,574 +0,0 @@
#include "lua_engine.h"
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
#include <cctype>
#include <cstring>
#include <cstdio>
#include <string>
namespace lua_engine {
struct Engine {
lua_State* L = nullptr;
std::vector<RowCtx*> ctx_stack;
std::vector<int> visiting_derived;
};
namespace {
Engine* g_engine = nullptr;
Engine* engine_from_state(lua_State* L) {
return *static_cast<Engine**>(lua_getextraspace(L));
}
RowCtx* current_ctx(lua_State* L) {
Engine* e = engine_from_state(L);
if (!e || e->ctx_stack.empty()) return nullptr;
return e->ctx_stack.back();
}
// ---------------------------------------------------------------------------
// Push de cell respetando tipo declarado:
// Int/Float -> number (integer si exacto)
// Bool -> boolean (true/false/1/0); en otro caso push string
// Date/String/Json/Auto -> string
// Si types_orig == nullptr -> heuristica: parse_number; si parsea -> number.
// ---------------------------------------------------------------------------
void push_typed(lua_State* L, const char* v, data_table::ColumnType t) {
if (!v || !*v) { lua_pushnil(L); return; }
using data_table::ColumnType;
using data_table::parse_number;
if (t == ColumnType::Int) {
double d;
if (parse_number(v, d)) {
long long iv = (long long)d;
if ((double)iv == d) lua_pushinteger(L, (lua_Integer)iv);
else lua_pushnumber (L, (lua_Number)d);
} else lua_pushstring(L, v);
return;
}
if (t == ColumnType::Float) {
double d;
if (parse_number(v, d)) {
long long iv = (long long)d;
if ((double)iv == d) lua_pushinteger(L, (lua_Integer)iv);
else lua_pushnumber (L, (lua_Number)d);
} else lua_pushstring(L, v);
return;
}
if (t == ColumnType::Bool) {
if (std::strcmp(v, "true") == 0 || std::strcmp(v, "1") == 0) lua_pushboolean(L, 1);
else if (std::strcmp(v, "false") == 0 || std::strcmp(v, "0") == 0) lua_pushboolean(L, 0);
else lua_pushstring(L, v);
return;
}
if (t == ColumnType::Auto) {
// Sin tipo declarado: heuristica. parse_number -> number, else string.
double d;
if (parse_number(v, d)) {
long long iv = (long long)d;
if ((double)iv == d) lua_pushinteger(L, (lua_Integer)iv);
else lua_pushnumber (L, (lua_Number)d);
} else lua_pushstring(L, v);
return;
}
// String / Date / Json
lua_pushstring(L, v);
}
// Fwd: para recursion en row_index.
std::string eval_internal(Engine* e, int id, const RowCtx& ctx, std::string* err_out);
int row_index(lua_State* L) {
Engine* eng = engine_from_state(L);
RowCtx* ctx = current_ctx(L);
if (!ctx) { lua_pushnil(L); return 1; }
using data_table::ColumnType;
auto get_orig_type = [&](int c) -> ColumnType {
if (ctx->types_orig && c < ctx->n_types_orig) return ctx->types_orig[c];
return ColumnType::Auto;
};
if (lua_type(L, 2) == LUA_TSTRING) {
const char* key = lua_tostring(L, 2);
if (ctx->name_to_col) {
auto it = ctx->name_to_col->find(key);
if (it != ctx->name_to_col->end()) {
int col = it->second;
push_typed(L, ctx->cells[ctx->row * ctx->orig_cols + col], get_orig_type(col));
return 1;
}
}
if (ctx->derived_name_to_idx && ctx->derived) {
auto it = ctx->derived_name_to_idx->find(key);
if (it != ctx->derived_name_to_idx->end()) {
int didx = it->second;
if (didx < 0 || didx >= (int)ctx->derived->size()) {
lua_pushnil(L); return 1;
}
// cycle check
for (int v : eng->visiting_derived) {
if (v == didx) { lua_pushnil(L); return 1; }
}
const auto& d = (*ctx->derived)[didx];
if (d.formula.empty()) {
// retipo puro
if (d.source_col < 0 || d.source_col >= ctx->orig_cols) {
lua_pushnil(L); return 1;
}
push_typed(L, ctx->cells[ctx->row * ctx->orig_cols + d.source_col], d.type);
} else if (d.lua_id < 0) {
lua_pushnil(L);
} else {
eng->visiting_derived.push_back(didx);
std::string err;
std::string r = eval_internal(eng, d.lua_id, *ctx, &err);
eng->visiting_derived.pop_back();
push_typed(L, r.c_str(), d.type);
}
return 1;
}
}
lua_pushnil(L);
return 1;
}
if (lua_type(L, 2) == LUA_TNUMBER) {
int idx = (int)lua_tointeger(L, 2);
if (idx >= 1 && idx <= ctx->orig_cols) {
int col = idx - 1;
push_typed(L, ctx->cells[ctx->row * ctx->orig_cols + col], get_orig_type(col));
return 1;
}
}
lua_pushnil(L);
return 1;
}
// --- fn.* builtins ---
int b_upper(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
std::string out(s);
for (char& c : out) if (c >= 'a' && c <= 'z') c -= 32;
lua_pushlstring(L, out.data(), out.size());
return 1;
}
int b_lower(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
std::string out(s);
for (char& c : out) if (c >= 'A' && c <= 'Z') c += 32;
lua_pushlstring(L, out.data(), out.size());
return 1;
}
int b_length(lua_State* L) {
if (lua_isnil(L, 1)) { lua_pushinteger(L, 0); return 1; }
const char* s = luaL_checkstring(L, 1);
lua_pushinteger(L, (lua_Integer)std::strlen(s));
return 1;
}
int b_substring(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
int start = (int)luaL_checkinteger(L, 2);
int len = (int)luaL_optinteger(L, 3, -1);
int slen = (int)std::strlen(s);
if (start < 1) start = 1;
if (start > slen) { lua_pushlstring(L, "", 0); return 1; }
int from = start - 1;
int take = (len < 0) ? slen - from : len;
if (from + take > slen) take = slen - from;
lua_pushlstring(L, s + from, take);
return 1;
}
int b_contains(lua_State* L) {
const char* h = luaL_checkstring(L, 1);
const char* n = luaL_checkstring(L, 2);
lua_pushboolean(L, std::strstr(h, n) != nullptr);
return 1;
}
int b_starts_with(lua_State* L) {
const char* h = luaL_checkstring(L, 1);
const char* n = luaL_checkstring(L, 2);
size_t ln = std::strlen(n);
lua_pushboolean(L, std::strncmp(h, n, ln) == 0);
return 1;
}
int b_ends_with(lua_State* L) {
const char* h = luaL_checkstring(L, 1);
const char* n = luaL_checkstring(L, 2);
size_t lh = std::strlen(h), ln = std::strlen(n);
lua_pushboolean(L, ln <= lh && std::strcmp(h + lh - ln, n) == 0);
return 1;
}
int b_replace(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
const char* find = luaL_checkstring(L, 2);
const char* repl = luaL_checkstring(L, 3);
std::string out;
size_t flen = std::strlen(find);
if (flen == 0) { lua_pushstring(L, s); return 1; }
for (const char* p = s; *p; ) {
if (std::strncmp(p, find, flen) == 0) { out += repl; p += flen; }
else { out += *p++; }
}
lua_pushlstring(L, out.data(), out.size());
return 1;
}
int b_trim(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') ++s;
const char* e = s + std::strlen(s);
while (e > s && (e[-1] == ' ' || e[-1] == '\t' || e[-1] == '\n' || e[-1] == '\r')) --e;
lua_pushlstring(L, s, e - s);
return 1;
}
int b_concat(lua_State* L) {
int n = lua_gettop(L);
std::string out;
for (int i = 1; i <= n; ++i) {
size_t sl = 0;
const char* s = luaL_tolstring(L, i, &sl);
out.append(s, sl);
lua_pop(L, 1);
}
lua_pushlstring(L, out.data(), out.size());
return 1;
}
int b_to_number(lua_State* L) {
if (lua_isnumber(L, 1)) { lua_pushvalue(L, 1); return 1; }
const char* s = luaL_checkstring(L, 1);
char* end = nullptr;
double v = std::strtod(s, &end);
if (end == s) { lua_pushnil(L); return 1; }
lua_pushnumber(L, v);
return 1;
}
int b_to_string(lua_State* L) { luaL_tolstring(L, 1, nullptr); return 1; }
int b_to_bool(lua_State* L) {
if (lua_isboolean(L, 1)) { lua_pushvalue(L, 1); return 1; }
const char* s = luaL_optstring(L, 1, "");
lua_pushboolean(L, std::strcmp(s, "true") == 0 || std::strcmp(s, "1") == 0);
return 1;
}
int b_is_null(lua_State* L) { lua_pushboolean(L, lua_isnil(L, 1)); return 1; }
int b_is_empty(lua_State* L) {
if (lua_isnil(L, 1)) { lua_pushboolean(L, 1); return 1; }
const char* s = luaL_optstring(L, 1, "");
lua_pushboolean(L, *s == 0);
return 1;
}
int b_coalesce(lua_State* L) {
int n = lua_gettop(L);
for (int i = 1; i <= n; ++i) {
if (!lua_isnil(L, i)) { lua_pushvalue(L, i); return 1; }
}
lua_pushnil(L);
return 1;
}
int b_parse_date(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
if (std::strlen(s) < 10) { lua_pushnil(L); return 1; }
int y, m, d;
if (std::sscanf(s, "%d-%d-%d", &y, &m, &d) != 3) { lua_pushnil(L); return 1; }
lua_createtable(L, 0, 3);
lua_pushinteger(L, y); lua_setfield(L, -2, "year");
lua_pushinteger(L, m); lua_setfield(L, -2, "month");
lua_pushinteger(L, d); lua_setfield(L, -2, "day");
return 1;
}
int b_year(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
int y; if (std::sscanf(s, "%d", &y) != 1) { lua_pushnil(L); return 1; }
lua_pushinteger(L, y); return 1;
}
int b_month(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
int y, m; if (std::sscanf(s, "%d-%d", &y, &m) != 2) { lua_pushnil(L); return 1; }
lua_pushinteger(L, m); return 1;
}
int b_day(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
int y, m, d; if (std::sscanf(s, "%d-%d-%d", &y, &m, &d) != 3) { lua_pushnil(L); return 1; }
lua_pushinteger(L, d); return 1;
}
void apply_sandbox(lua_State* L) {
const char* nuke[] = { "io", "require", "loadfile", "dofile", "load",
"package", "debug", nullptr };
for (int i = 0; nuke[i]; ++i) {
lua_pushnil(L);
lua_setglobal(L, nuke[i]);
}
lua_getglobal(L, "os");
if (lua_istable(L, -1)) {
lua_createtable(L, 0, 4);
const char* keep[] = {"date", "time", "difftime", "clock", nullptr};
for (int i = 0; keep[i]; ++i) {
lua_getfield(L, -2, keep[i]);
lua_setfield(L, -2, keep[i]);
}
lua_setglobal(L, "os");
}
lua_pop(L, 1);
}
void register_builtins(lua_State* L) {
lua_createtable(L, 0, 24);
#define R(name, fn) lua_pushcfunction(L, fn); lua_setfield(L, -2, name);
R("upper", b_upper);
R("lower", b_lower);
R("length", b_length);
R("substring", b_substring);
R("contains", b_contains);
R("starts_with", b_starts_with);
R("ends_with", b_ends_with);
R("replace", b_replace);
R("trim", b_trim);
R("concat", b_concat);
R("to_number", b_to_number);
R("to_string", b_to_string);
R("to_bool", b_to_bool);
R("is_null", b_is_null);
R("is_empty", b_is_empty);
R("coalesce", b_coalesce);
R("parse_date", b_parse_date);
R("year", b_year);
R("month", b_month);
R("day", b_day);
#undef R
lua_setglobal(L, "fn");
}
void install_row_metatable(lua_State* L) {
luaL_newmetatable(L, "fn_row_meta");
lua_pushcfunction(L, row_index);
lua_setfield(L, -2, "__index");
lua_pop(L, 1);
}
// ---------------------------------------------------------------------------
// Preprocesador: [col] -> row["col"] respetando strings y comentarios.
// Auto-prepend `return` si la formula es expresion suelta.
// ---------------------------------------------------------------------------
bool ident_start(unsigned char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c >= 0x80;
}
// Para nombres de cols dentro de [name]: permite espacios para "col with space"
// y '.' para futuro `alias.col` post-join (fase 9 — issue 0078).
bool ident_cont(unsigned char c) {
return ident_start(c) || (c >= '0' && c <= '9') || c == ' ' || c == '.';
}
// Para boundary de keywords Lua: NO permite espacio.
bool word_char(unsigned char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '_' || c >= 0x80;
}
bool kw_at(const std::string& s, size_t i, const char* kw) {
size_t k = std::strlen(kw);
if (i + k > s.size()) return false;
if (s.compare(i, k, kw) != 0) return false;
if (i + k == s.size()) return true;
unsigned char nc = (unsigned char)s[i + k];
return !word_char(nc);
}
bool needs_auto_return(const std::string& body) {
size_t i = 0;
while (i < body.size()) {
char c = body[i];
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { ++i; continue; }
// skip short comment
if (c == '-' && i + 1 < body.size() && body[i+1] == '-') {
// long comment?
if (i + 3 < body.size() && body[i+2] == '[' && body[i+3] == '[') {
size_t j = i + 4;
while (j + 1 < body.size() && !(body[j] == ']' && body[j+1] == ']')) ++j;
i = (j + 1 < body.size()) ? j + 2 : body.size();
continue;
}
while (i < body.size() && body[i] != '\n') ++i;
continue;
}
break;
}
if (i >= body.size()) return false;
const char* kws[] = {"return","if","for","while","do","local","repeat","function", nullptr};
for (int k = 0; kws[k]; ++k) if (kw_at(body, i, kws[k])) return false;
return true;
}
std::string brackets_pass(const std::string& src) {
std::string out;
out.reserve(src.size() + 16);
size_t i = 0;
while (i < src.size()) {
char c = src[i];
// strings
if (c == '"' || c == '\'') {
char q = c;
out += c; ++i;
while (i < src.size()) {
char d = src[i];
out += d; ++i;
if (d == '\\' && i < src.size()) { out += src[i++]; continue; }
if (d == q) break;
if (d == '\n') break;
}
continue;
}
// comentario corto / largo
if (c == '-' && i + 1 < src.size() && src[i+1] == '-') {
// long: --[[ ... ]]
if (i + 3 < src.size() && src[i+2] == '[' && src[i+3] == '[') {
out.append(src, i, 4); i += 4;
while (i + 1 < src.size() && !(src[i] == ']' && src[i+1] == ']')) {
out += src[i++];
}
if (i + 1 < src.size()) { out += src[i++]; out += src[i++]; }
continue;
}
// short
while (i < src.size() && src[i] != '\n') { out += src[i++]; }
continue;
}
// long string [[ ... ]]
if (c == '[' && i + 1 < src.size() && src[i+1] == '[') {
out.append(src, i, 2); i += 2;
while (i + 1 < src.size() && !(src[i] == ']' && src[i+1] == ']')) {
out += src[i++];
}
if (i + 1 < src.size()) { out += src[i++]; out += src[i++]; }
continue;
}
// bracket col-ref [name]
if (c == '[') {
// peek if next is valid ident_start
if (i + 1 < src.size() && ident_start((unsigned char)src[i+1])) {
size_t j = i + 1;
while (j < src.size() && src[j] != ']' && src[j] != '\n') {
if (!ident_cont((unsigned char)src[j])) { j = std::string::npos; break; }
++j;
}
if (j != std::string::npos && j < src.size() && src[j] == ']') {
std::string name(src, i + 1, j - i - 1);
// trim trailing space
while (!name.empty() && name.back() == ' ') name.pop_back();
out += "row[\"";
out += name;
out += "\"]";
i = j + 1;
continue;
}
}
}
out += c;
++i;
}
return out;
}
} // anon
std::string preprocess(const std::string& body) {
std::string pre = brackets_pass(body);
if (needs_auto_return(pre)) return "return " + pre;
return pre;
}
namespace {
std::string eval_internal(Engine* e, int id, const RowCtx& ctx, std::string* err_out) {
if (!e || !e->L || id < 0) {
if (err_out) *err_out = "invalid handle";
return "";
}
lua_State* L = e->L;
e->ctx_stack.push_back(const_cast<RowCtx*>(&ctx));
lua_rawgeti(L, LUA_REGISTRYINDEX, id);
lua_newuserdata(L, 1);
luaL_setmetatable(L, "fn_row_meta");
int rc = lua_pcall(L, 1, 1, 0);
e->ctx_stack.pop_back();
if (rc != LUA_OK) {
if (err_out) *err_out = lua_tostring(L, -1) ? lua_tostring(L, -1) : "runtime error";
lua_pop(L, 1);
return "";
}
std::string out;
if (lua_isnil(L, -1)) out = "";
else {
size_t n = 0;
const char* s = luaL_tolstring(L, -1, &n);
out.assign(s, n);
lua_pop(L, 1);
}
lua_pop(L, 1);
return out;
}
} // anon
Engine* get() {
if (g_engine) return g_engine;
g_engine = new Engine();
g_engine->L = luaL_newstate();
luaL_openlibs(g_engine->L);
*static_cast<Engine**>(lua_getextraspace(g_engine->L)) = g_engine;
apply_sandbox(g_engine->L);
register_builtins(g_engine->L);
install_row_metatable(g_engine->L);
return g_engine;
}
void shutdown() {
if (!g_engine) return;
lua_close(g_engine->L);
delete g_engine;
g_engine = nullptr;
}
int compile(Engine* e, const std::string& body, std::string* err_out) {
if (!e || !e->L) { if (err_out) *err_out = "engine null"; return -1; }
lua_State* L = e->L;
std::string final_body = preprocess(body);
std::string wrapped = "return function(row)\n" + final_body + "\nend";
if (luaL_loadbufferx(L, wrapped.data(), wrapped.size(), "formula", "t") != LUA_OK) {
if (err_out) *err_out = lua_tostring(L, -1) ? lua_tostring(L, -1) : "parse error";
lua_pop(L, 1);
return -1;
}
if (lua_pcall(L, 0, 1, 0) != LUA_OK) {
if (err_out) *err_out = lua_tostring(L, -1) ? lua_tostring(L, -1) : "compile error";
lua_pop(L, 1);
return -1;
}
if (!lua_isfunction(L, -1)) {
if (err_out) *err_out = "formula did not produce a function";
lua_pop(L, 1);
return -1;
}
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
return ref;
}
void release(Engine* e, int id) {
if (!e || !e->L || id < 0) return;
luaL_unref(e->L, LUA_REGISTRYINDEX, id);
}
std::string eval(Engine* e, int id, const RowCtx& ctx, std::string* err_out) {
return eval_internal(e, id, ctx, err_out);
}
lua_State* raw_state() {
Engine* e = get();
return e ? e->L : nullptr;
}
} // namespace lua_engine
@@ -1,61 +0,0 @@
// Lua 5.4 wrapper para formulas de columnas custom del playground tables.
//
// Features:
// - Sandbox medio: io/require/dofile fuera; os reducido a date/time/diff/clock.
// - Builtins fn.* (~20 funciones).
// - Sintaxis [col_name] preprocesada a row["col_name"].
// - Auto-`return` si la formula es expresion suelta sin keyword inicial.
// - Type-aware push: row.x devuelve number si la col es Int/Float, boolean
// si Bool, string en el resto (Date/String/Json). Nil si vacia.
// - UTF-8 ok en nombres de columnas dentro de [].
// - Comentarios y string literals preservados por el preprocesador.
// - Llamadas recursivas: un derived col puede referenciar a otro derived col;
// ciclos cortados con nil.
#pragma once
#include "data_table_logic.h"
#include <string>
#include <unordered_map>
#include <vector>
// Forward declaration del C struct de Lua (definido en lua.h).
struct lua_State;
namespace lua_engine {
struct Engine;
Engine* get();
void shutdown();
int compile(Engine* e, const std::string& body, std::string* err_out);
void release(Engine* e, int id);
struct RowCtx {
const char* const* cells = nullptr;
int orig_cols = 0;
int row = 0;
const std::vector<std::string>* header_names = nullptr;
const std::unordered_map<std::string,int>* name_to_col = nullptr;
// Tipos declarados/auto-detect de las cols originales. nullptr -> heuristica.
const data_table::ColumnType* types_orig = nullptr;
int n_types_orig = 0;
// Derived cols + lookup por nombre (incluye retipo puro y formulas).
const std::vector<data_table::DerivedColumn>* derived = nullptr;
const std::unordered_map<std::string,int>* derived_name_to_idx = nullptr;
};
std::string eval(Engine* e, int id, const RowCtx& ctx, std::string* err_out);
// Helper expuesto para tests: preprocesa `[col]` -> `row["col"]` respetando
// strings y comentarios. Tambien aplica auto-return.
std::string preprocess(const std::string& body);
// Acceso al lua_State subyacente. Uso restringido: tql.cpp parsea chunks
// (return { ... }) y walks tablas. NO usar para nada que rompa el sandbox.
::lua_State* raw_state();
} // namespace lua_engine
@@ -1,191 +0,0 @@
#include "app_base.h"
#include "imgui.h"
#include "core/logger.h"
#include "data_table.h"
#include <cstdio>
#include <cstring>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace {
// ---------------------------------------------------------------------------
// Dataset generador. Filas se generan con valores deterministas en funcion del
// indice (semilla = i). Strings repetidas (lang/domain/purity/tested) usan
// interned literals -> sin coste de memoria por fila.
// ---------------------------------------------------------------------------
struct Dataset {
int rows = 0;
int cols = 10;
std::vector<std::string> backing; // dynamic strings (name, version, deps, size, cov, date)
std::vector<const char*> cells; // row-major pointers
};
const char* const* dataset_cells(const Dataset& d) { return d.cells.data(); }
std::shared_ptr<Dataset> build_dataset(int rows) {
auto d = std::make_shared<Dataset>();
d->rows = rows;
d->cols = 10;
static const char* langs[] = {"go", "py", "cpp", "bash", "ts"};
static const char* domains[] = {"core", "viz", "infra", "finance", "notebook", "shell"};
static const char* puritys[] = {"pure", "impure"};
static const char* bools[] = {"true", "false"};
// Reserve antes de pushear -> punteros .c_str() estables.
d->backing.reserve((size_t)rows * 6 + 16);
d->cells.reserve((size_t)rows * 10);
auto add = [&](const std::string& s) -> const char* {
d->backing.push_back(s);
return d->backing.back().c_str();
};
char buf[40];
for (int i = 0; i < rows; ++i) {
std::snprintf(buf, sizeof(buf), "fn_%07d", i);
const char* name = add(buf);
const char* lang = langs[i % 5];
const char* domain = domains[i % 6];
const char* purity = puritys[i % 2];
std::snprintf(buf, sizeof(buf), "%d", (i % 5) + 1);
const char* vmaj = add(buf);
std::snprintf(buf, sizeof(buf), "%d", i % 7);
const char* deps = add(buf);
std::snprintf(buf, sizeof(buf), "%.2f", ((i * 31) % 10000) / 100.0);
const char* size = add(buf);
std::snprintf(buf, sizeof(buf), "%.1f", (i % 1001) / 10.0);
const char* cov = add(buf);
const char* tst = bools[(i * 3) % 2];
int y = 2024 + (i % 3);
int m = 1 + (i % 12);
int day = 1 + (i % 28);
std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d", y, m, day);
const char* dt = add(buf);
d->cells.push_back(name);
d->cells.push_back(lang);
d->cells.push_back(domain);
d->cells.push_back(purity);
d->cells.push_back(vmaj);
d->cells.push_back(deps);
d->cells.push_back(size);
d->cells.push_back(cov);
d->cells.push_back(tst);
d->cells.push_back(dt);
}
return d;
}
std::shared_ptr<Dataset>& current_dataset() {
static std::shared_ptr<Dataset> ds;
if (!ds) ds = build_dataset(100);
return ds;
}
} // namespace
void render() {
static data_table::State st;
if (ImGui::Begin("Tables Playground - data_table v0.5")) {
ImGui::TextWrapped(
"v0.5: + en chip-row anade filtro a cualquier col. Show stats muestra "
"0/uniq/mean/min/max por header. Clipper virtualiza render -> 1M filas a 60 FPS.");
ImGui::Separator();
ImGui::Text("Dataset size:");
ImGui::SameLine();
const int sizes[] = {100, 10000, 100000, 1000000};
const char* labels[] = {"100", "10K", "100K", "1M"};
for (size_t i = 0; i < sizeof(sizes)/sizeof(sizes[0]); ++i) {
if (i > 0) ImGui::SameLine();
bool is_active = (current_dataset()->rows == sizes[i]);
if (is_active) ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(80, 120, 80, 255));
if (ImGui::SmallButton(labels[i])) {
current_dataset() = build_dataset(sizes[i]);
st = data_table::State{}; // reset filtros/sort/orden al cambiar dataset
}
if (is_active) ImGui::PopStyleColor();
}
ImGui::SameLine();
ImGui::TextDisabled("(actual: %d filas)", current_dataset()->rows);
ImGui::Separator();
static const char* headers[] = {
"name", "lang", "domain", "purity",
"version_major", "deps_count", "size_kb", "coverage_pct",
"tested", "updated_at"
};
static const data_table::ColumnType types[] = {
data_table::ColumnType::String, // name
data_table::ColumnType::String, // lang
data_table::ColumnType::String, // domain
data_table::ColumnType::String, // purity
data_table::ColumnType::Int, // version_major
data_table::ColumnType::Int, // deps_count
data_table::ColumnType::Float, // size_kb
data_table::ColumnType::Float, // coverage_pct
data_table::ColumnType::Bool, // tested
data_table::ColumnType::Date, // updated_at
};
// Tabla extra para demo de joins (fase 9).
static const char* lang_info_cells[] = {
"go", "compiled", "2009",
"py", "interp", "1991",
"rust", "compiled", "2010",
"ts", "interp", "2012",
"bash", "shell", "1989",
"lua", "interp", "1993",
};
static data_table::TableInput lang_info;
if (lang_info.name.empty()) {
lang_info.name = "lang_info";
lang_info.headers = {"lang", "family", "year"};
lang_info.types = {data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::Int};
lang_info.cells = lang_info_cells;
lang_info.rows = 6;
lang_info.cols = 3;
}
const auto& d = *current_dataset();
static data_table::TableInput main_t;
main_t.name = "fn_registry";
main_t.headers = {"name", "lang", "domain", "purity",
"version_major", "deps_count", "size_kb", "coverage_pct",
"tested", "updated_at"};
main_t.types = std::vector<data_table::ColumnType>(types, types + 10);
main_t.cells = dataset_cells(d);
main_t.rows = d.rows;
main_t.cols = d.cols;
std::vector<data_table::TableInput> tables = { main_t, lang_info };
data_table::render("##bigdata", tables, st);
}
ImGui::End();
}
#ifndef FN_TEST_BUILD
int main() {
return fn::run_app({
.title = "Tables Playground",
.width = 1400,
.height = 900,
.about = {.name = "tables_playground",
.version = "0.5.0",
.description = "Playground data_table: + add filter, stats por columna, "
"clipper para datasets de millones."},
.log = {.file_path = "tables_playground.log",
.level = static_cast<int>(fn_log::Level::Info)}
}, render);
}
#endif
File diff suppressed because it is too large Load Diff
@@ -1,913 +0,0 @@
#include "tql.h"
#include "lua_engine.h"
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <unordered_map>
namespace tql {
using namespace data_table;
namespace {
int find_orig_col(const std::vector<std::string>& headers, const std::string& name) {
for (size_t i = 0; i < headers.size(); ++i) if (headers[i] == name) return (int)i;
return -1;
}
int find_derived_idx(const std::vector<DerivedColumn>& d, const std::string& name) {
for (size_t i = 0; i < d.size(); ++i) if (d[i].name == name) return (int)i;
return -1;
}
Op parse_op(const std::string& s) {
if (s == "=") return Op::Eq;
if (s == "!=") return Op::Neq;
if (s == ">") return Op::Gt;
if (s == ">=") return Op::Gte;
if (s == "<") return Op::Lt;
if (s == "<=") return Op::Lte;
if (s == "contains") return Op::Contains;
if (s == "!contains") return Op::NotContains;
if (s == "starts") return Op::StartsWith;
if (s == "ends") return Op::EndsWith;
return Op::Eq;
}
std::string lua_to_string(lua_State* L, int idx) {
if (lua_isnil(L, idx)) return "";
if (lua_isboolean(L, idx)) return lua_toboolean(L, idx) ? "true" : "false";
size_t n = 0;
const char* s = luaL_tolstring(L, idx, &n);
std::string out(s, n);
lua_pop(L, 1);
return out;
}
} // anon
std::string lua_string_literal(const std::string& s) {
std::string out;
out.reserve(s.size() + 4);
out += '"';
for (char c : s) {
switch (c) {
case '\\': out += "\\\\"; break;
case '"': out += "\\\""; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
default:
if ((unsigned char)c < 0x20) {
char b[8]; std::snprintf(b, sizeof(b), "\\%d", (unsigned char)c);
out += b;
} else out += c;
}
}
out += '"';
return out;
}
std::string color_to_hex(unsigned int c) {
unsigned int r = c & 0xFF;
unsigned int g = (c >> 8) & 0xFF;
unsigned int b = (c >> 16) & 0xFF;
unsigned int a = (c >> 24) & 0xFF;
char buf[16];
if (a == 0xFF) std::snprintf(buf, sizeof(buf), "#%02x%02x%02x", r, g, b);
else std::snprintf(buf, sizeof(buf), "#%02x%02x%02x%02x", r, g, b, a);
return buf;
}
unsigned int hex_to_color(const std::string& s) {
if (s.size() < 7 || s[0] != '#') return 0xFFFFFFFF;
auto hex2 = [&](size_t i) -> unsigned int {
unsigned int v = 0;
if (i + 1 < s.size()) std::sscanf(s.c_str() + i, "%2x", &v);
return v;
};
unsigned int r = hex2(1), g = hex2(3), b = hex2(5);
unsigned int a = (s.size() >= 9) ? hex2(7) : 0xFF;
return r | (g << 8) | (b << 16) | (a << 24);
}
ColumnType column_type_from_string(const std::string& s) {
if (s == "string") return ColumnType::String;
if (s == "int") return ColumnType::Int;
if (s == "float") return ColumnType::Float;
if (s == "bool") return ColumnType::Bool;
if (s == "date") return ColumnType::Date;
if (s == "json") return ColumnType::Json;
return ColumnType::Auto;
}
// Helper: header del Stage 0 dado un col idx eff. Para stages 1+ no aplica
// (los stage outputs tienen sus propios headers).
namespace {
const char* agg_fn_token(AggFn f) {
switch (f) {
case AggFn::Count: return "count";
case AggFn::Sum: return "sum";
case AggFn::Avg: return "avg";
case AggFn::Min: return "min";
case AggFn::Max: return "max";
case AggFn::Distinct: return "distinct";
case AggFn::Stddev: return "stddev";
case AggFn::Median: return "median";
case AggFn::P25: return "p25";
case AggFn::P75: return "p75";
case AggFn::P90: return "p90";
case AggFn::P99: return "p99";
case AggFn::Percentile: return "percentile";
}
return "?";
}
AggFn agg_fn_from_string(const std::string& s) {
if (s == "count") return AggFn::Count;
if (s == "sum") return AggFn::Sum;
if (s == "avg") return AggFn::Avg;
if (s == "min") return AggFn::Min;
if (s == "max") return AggFn::Max;
if (s == "distinct") return AggFn::Distinct;
if (s == "stddev") return AggFn::Stddev;
if (s == "median") return AggFn::Median;
if (s == "p25") return AggFn::P25;
if (s == "p75") return AggFn::P75;
if (s == "p90") return AggFn::P90;
if (s == "p99") return AggFn::P99;
if (s == "percentile") return AggFn::Percentile;
return AggFn::Count;
}
} // anon
std::string emit(const State& state,
const std::vector<std::string>& headers,
const std::vector<ColumnType>& types)
{
int orig_cols = (int)headers.size();
const Stage& raw = state.raw();
int eff_cols = orig_cols + (int)raw.derived.size();
// Build effective headers + types (same indexing as col_visible/order)
std::vector<std::string> eff_headers(eff_cols);
std::vector<ColumnType> eff_types(eff_cols);
for (int c = 0; c < orig_cols; ++c) {
eff_headers[c] = headers[c];
eff_types[c] = (c < (int)types.size()) ? types[c] : ColumnType::Auto;
}
for (int k = 0; k < (int)raw.derived.size(); ++k) {
eff_headers[orig_cols + k] = raw.derived[k].name;
eff_types[orig_cols + k] = raw.derived[k].type;
}
// Build order positions: col_idx -> visual order (1-based)
std::unordered_map<int, int> order_pos;
for (size_t i = 0; i < state.col_order.size(); ++i) {
order_pos[state.col_order[i]] = (int)i + 1;
}
auto emit_filter_block = [&](const std::vector<Filter>& filters,
const std::vector<std::string>& stage_headers,
const char* indent) -> std::string {
if (filters.empty()) return {};
std::string s;
s += indent; s += "filter = {\n";
for (const auto& f : filters) {
std::string col_name = (f.col >= 0 && f.col < (int)stage_headers.size())
? stage_headers[f.col] : "";
s += indent; s += " {";
s += lua_string_literal(op_label(f.op));
s += ", ";
s += lua_string_literal(col_name);
s += ", ";
s += lua_string_literal(f.value);
s += "},\n";
}
s += indent; s += "},\n";
return s;
};
auto emit_sort_block = [&](const std::vector<SortClause>& sorts,
const char* indent) -> std::string {
if (sorts.empty()) return {};
std::string s;
s += indent; s += "sort = {\n";
for (const auto& sc : sorts) {
s += indent; s += " {";
s += lua_string_literal(sc.desc ? "desc" : "asc");
s += ", ";
s += lua_string_literal(sc.col);
s += "},\n";
}
s += indent; s += "},\n";
return s;
};
std::string out;
out += "-- TQL v1 (Table Query Language). Round-trip de State <-> Lua.\n";
out += "-- Schema:\n";
out += "-- version = 1 -- bump si breaking change\n";
out += "-- display = \"table\" -- table|bar|line|pie (futuro)\n";
out += "-- stages = { stage0, stage1, ... } -- pipeline; stage 0 = Raw\n";
out += "-- columns = { {name,type,visible,order,color_rules}, ... }\n";
out += "--\n";
out += "-- Stage 0 (Raw): filter + expressions + sort\n";
out += "-- Stage N (Grouped): filter + breakout + aggregation + sort\n";
out += "--\n";
out += "-- filter: {{op, col, val}, ...} op in =,!=,>,>=,<,<=,contains,!contains,starts,ends\n";
out += "-- expressions: {[name] = \"lua_body\"} ej: [\"total\"] = \"return [a] + [b]\"\n";
out += "-- breakout: {\"col1\", \"col2\"} group by\n";
out += "-- aggregation: {{fn, col, arg?}, ...} fn in count,sum,avg,min,max,distinct,stddev,median,p25,p75,p90,p99,percentile\n";
out += "-- sort: {{dir, col}, ...} dir in asc,desc\n";
out += "return {\n";
out += " version = 1,\n";
out += " display = ";
out += lua_string_literal(view_mode_token(state.display));
out += ",\n";
if (!state.main_source.empty()) {
out += " main_source = ";
out += lua_string_literal(state.main_source);
out += ",\n";
}
// joins (antes de stages, materializa input)
if (!state.joins.empty()) {
out += " joins = {\n";
for (const auto& jn : state.joins) {
out += " {alias = " + lua_string_literal(jn.alias);
out += ", source = " + lua_string_literal(jn.source);
out += ", strategy = " + lua_string_literal(join_strategy_token(jn.strategy));
out += ", on = {";
for (size_t i = 0; i < jn.on.size(); ++i) {
if (i) out += ", ";
out += "{" + lua_string_literal(jn.on[i].first) + ", "
+ lua_string_literal(jn.on[i].second) + "}";
}
out += "}";
if (!jn.fields.empty()) {
out += ", fields = {";
for (size_t i = 0; i < jn.fields.size(); ++i) {
if (i) out += ", ";
out += lua_string_literal(jn.fields[i]);
}
out += "}";
}
out += "},\n";
}
out += " },\n";
}
out += " stages = {\n";
// Recorre todos los stages; stage 0 tiene formato Raw (filter+expr+sort),
// stages 1+ tienen formato Grouped (filter+breakout+aggregation+sort).
// Headers para resolver col indices de filters/sorts se computan stage por
// stage simulando la cadena.
std::vector<std::string> cur_headers = headers; // stage input headers
// Para stage 0 raw, los headers incluyen orig + derived.
// Construye cur_headers iniciales (= orig); derived se anaden al pasar stage 0.
for (int si = 0; si < (int)state.stages.size(); ++si) {
const Stage& stg = state.stages[si];
out += " {\n";
if (si == 0) {
// Stage 0: orig headers + derived seran disponibles tras expressions.
// Para los filter col indices, asumimos van con cur_headers = orig.
// (data_table.cpp solo aplica filters a orig cols al guardar; si en
// futuro stage 0 admite filter sobre derived, se traduce a name.)
std::vector<std::string> s0_headers = headers;
// Filters
out += emit_filter_block(stg.filters, s0_headers, " ");
// Expressions
if (!stg.derived.empty()) {
bool any = false;
for (const auto& d : stg.derived) if (!d.formula.empty()) { any = true; break; }
if (any) {
out += " expressions = {\n";
for (const auto& d : stg.derived) {
if (d.formula.empty()) continue;
out += " [";
out += lua_string_literal(d.name);
out += "] = ";
out += lua_string_literal(d.formula);
out += ",\n";
}
out += " },\n";
}
}
// Sort (sort.col es string en nuevo modelo).
out += emit_sort_block(stg.sorts, " ");
// Avanza cur_headers para siguiente stage: orig + derived.
for (const auto& d : stg.derived) cur_headers.push_back(d.name);
} else {
// Stage 1+: filter (sobre output del previo, cur_headers).
out += emit_filter_block(stg.filters, cur_headers, " ");
// breakout
if (!stg.breakouts.empty()) {
out += " breakout = {";
for (size_t i = 0; i < stg.breakouts.size(); ++i) {
if (i > 0) out += ", ";
out += lua_string_literal(stg.breakouts[i]);
}
out += "},\n";
}
// aggregation
if (!stg.aggregations.empty()) {
out += " aggregation = {\n";
for (const auto& a : stg.aggregations) {
out += " {";
out += lua_string_literal(agg_fn_token(a.fn));
if (a.fn != AggFn::Count) {
out += ", ";
out += lua_string_literal(a.col);
}
if (a.fn == AggFn::Percentile) {
char buf[32]; std::snprintf(buf, sizeof(buf), "%g", a.arg);
out += ", "; out += buf;
}
out += "},\n";
}
out += " },\n";
}
// sort
out += emit_sort_block(stg.sorts, " ");
// Avanza cur_headers para siguiente stage: breakouts + agg aliases.
std::vector<std::string> next;
for (const auto& b : stg.breakouts) next.push_back(b);
for (const auto& a : stg.aggregations) next.push_back(aggregation_alias(a));
cur_headers = std::move(next);
}
out += " },\n";
}
out += " },\n";
// columns (per-col render config) — siempre referidas a los effective cols
// del STAGE 0 (asumimos viz state para stage 0 / raw). Renderizar columns
// por cada stage no aporta v1.
out += " columns = {\n";
for (int c = 0; c < eff_cols; ++c) {
out += " {";
out += "name = " + lua_string_literal(eff_headers[c]);
out += ", type = " + lua_string_literal(column_type_name(eff_types[c]));
bool vis = (c < (int)state.col_visible.size()) ? state.col_visible[c] : true;
out += std::string(", visible = ") + (vis ? "true" : "false");
int order = order_pos.count(c) ? order_pos[c] : c + 1;
out += ", order = " + std::to_string(order);
// color rules for this col
bool first = true;
for (const auto& cr : state.color_rules) {
if (cr.col != c) continue;
if (first) { out += ", color_rules = {"; first = false; }
else { out += ", "; }
out += "{equals = " + lua_string_literal(cr.equals);
out += ", color = " + lua_string_literal(color_to_hex(cr.color)) + "}";
}
if (!first) out += "}";
out += "},\n";
}
out += " },\n";
// views (extra viz panels — viz adicional sobre mismos stages)
auto emit_view = [&](const VizPanel& p) -> std::string {
std::string s = " {";
s += "display = " + lua_string_literal(view_mode_token(p.display));
if (!p.config.x_col.empty()) s += ", x_col = " + lua_string_literal(p.config.x_col);
if (!p.config.cat_col.empty()) s += ", cat_col = " + lua_string_literal(p.config.cat_col);
if (!p.config.size_col.empty()) s += ", size_col = "+ lua_string_literal(p.config.size_col);
if (!p.config.y_cols.empty()) {
s += ", y_cols = {";
for (size_t i = 0; i < p.config.y_cols.size(); ++i) {
if (i) s += ", ";
s += lua_string_literal(p.config.y_cols[i]);
}
s += "}";
}
if (p.config.primary_color != 0)
s += ", color = " + lua_string_literal(color_to_hex(p.config.primary_color));
if (p.config.hist_bins > 0)
s += ", hist_bins = " + std::to_string(p.config.hist_bins);
if (p.config.pie_radius > 0)
s += ", pie_radius = " + std::to_string(p.config.pie_radius);
if (!p.config.show_legend) s += ", show_legend = false";
if (p.config.show_markers) s += ", show_markers = true";
if (p.config.locked) s += ", locked = true";
s += "},\n";
return s;
};
out += " views = {\n";
// Panel 0 = main viz
VizPanel main_p;
main_p.display = state.display;
main_p.config = state.viz_config;
out += emit_view(main_p);
for (const auto& p : state.extra_panels) out += emit_view(p);
out += " },\n";
out += " visualization_settings = {},\n";
out += "}\n";
return out;
}
bool apply(const std::string& lua_text, State& state,
const std::vector<std::string>& headers,
const std::vector<ColumnType>& /*types*/,
const char* const* cells, int rows, int orig_cols,
std::string* err)
{
std::vector<std::string> warns;
auto warn = [&](const std::string& m) { warns.push_back(m); };
auto finish_with_warns = [&](bool ok) -> bool {
if (err && !warns.empty()) {
std::string j;
for (size_t i = 0; i < warns.size(); ++i) {
if (i) j += "; ";
j += warns[i];
}
*err = j;
}
return ok;
};
lua_State* L = lua_engine::raw_state();
if (!L) { if (err) *err = "lua engine null"; return false; }
if (luaL_loadbufferx(L, lua_text.data(), lua_text.size(), "tql", "t") != LUA_OK) {
if (err) *err = lua_tostring(L, -1) ? lua_tostring(L, -1) : "load error";
lua_pop(L, 1);
return false;
}
if (lua_pcall(L, 0, 1, 0) != LUA_OK) {
if (err) *err = lua_tostring(L, -1) ? lua_tostring(L, -1) : "exec error";
lua_pop(L, 1);
return false;
}
if (!lua_istable(L, -1)) {
if (err) *err = "TQL root must be a table";
lua_pop(L, 1);
return false;
}
// main_source
lua_getfield(L, -1, "main_source");
if (lua_isstring(L, -1)) state.main_source = lua_tostring(L, -1);
else state.main_source.clear();
lua_pop(L, 1);
// display
lua_getfield(L, -1, "display");
if (lua_isstring(L, -1)) {
std::string d = lua_tostring(L, -1);
ViewMode m = view_mode_from_token(d.c_str());
state.display = m;
if (d != "table" && std::strcmp(view_mode_token(m), d.c_str()) != 0) {
warn("unknown display \"" + d + "\" (defaulting to table)");
}
}
lua_pop(L, 1);
// Validar version.
lua_getfield(L, -1, "version");
if (lua_isnil(L, -1)) {
warn("version missing (assuming 1)");
} else if (!lua_isnumber(L, -1)) {
if (err) *err = "version must be a number";
lua_pop(L, 2);
return false;
} else {
int v = (int)lua_tointeger(L, -1);
if (v != 1) {
char buf[64]; std::snprintf(buf, sizeof(buf), "unsupported TQL version %d (expected 1)", v);
if (err) *err = buf;
lua_pop(L, 2);
return false;
}
}
lua_pop(L, 1);
// Reset partes mutables. Liberar lua_ids antes.
for (auto& s : state.stages) {
for (auto& d : s.derived) {
if (d.lua_id >= 0) lua_engine::release(lua_engine::get(), d.lua_id);
}
}
state.stages.clear();
state.active_stage = 0;
state.color_rules.clear();
// ---- Walk joins[] ----
state.joins.clear();
lua_getfield(L, -1, "joins");
if (lua_istable(L, -1)) {
int nj = (int)lua_rawlen(L, -1);
for (int i = 1; i <= nj; ++i) {
lua_rawgeti(L, -1, i);
if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }
Join jn;
lua_getfield(L, -1, "alias");
if (lua_isstring(L, -1)) jn.alias = lua_tostring(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "source");
if (lua_isstring(L, -1)) jn.source = lua_tostring(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "strategy");
if (lua_isstring(L, -1)) jn.strategy = join_strategy_from_token(lua_tostring(L, -1));
lua_pop(L, 1);
lua_getfield(L, -1, "on");
if (lua_istable(L, -1)) {
int on_n = (int)lua_rawlen(L, -1);
for (int k = 1; k <= on_n; ++k) {
lua_rawgeti(L, -1, k);
if (lua_istable(L, -1) && lua_rawlen(L, -1) >= 2) {
lua_rawgeti(L, -1, 1); std::string a = lua_to_string(L, -1); lua_pop(L, 1);
lua_rawgeti(L, -1, 2); std::string b = lua_to_string(L, -1); lua_pop(L, 1);
jn.on.push_back({a, b});
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
lua_getfield(L, -1, "fields");
if (lua_istable(L, -1)) {
int fn_n = (int)lua_rawlen(L, -1);
for (int k = 1; k <= fn_n; ++k) {
lua_rawgeti(L, -1, k);
if (lua_isstring(L, -1)) jn.fields.emplace_back(lua_tostring(L, -1));
lua_pop(L, 1);
}
}
lua_pop(L, 1);
state.joins.push_back(jn);
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// ---- Walk stages[] ----
lua_getfield(L, -1, "stages");
if (lua_istable(L, -1)) {
int n_stages = (int)lua_rawlen(L, -1);
// Headers efectivos por stage para resolver filter/sort col indices.
std::vector<std::string> cur_headers = headers;
for (int si = 1; si <= n_stages; ++si) {
lua_rawgeti(L, -1, si);
if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }
Stage stg;
// Stage 0 expressions (solo aplica a si == 1, pero permitimos en
// cualquier stage por simetria — el UI no las expone en stages 1+).
lua_getfield(L, -1, "expressions");
if (lua_istable(L, -1)) {
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if (lua_isstring(L, -2) && lua_isstring(L, -1)) {
std::string name = lua_tostring(L, -2);
std::string formula = lua_tostring(L, -1);
std::string cerr;
int id = lua_engine::compile(lua_engine::get(), formula, &cerr);
DerivedColumn d;
d.source_col = -1;
d.name = name;
d.formula = formula;
d.lua_id = id;
d.compile_error = (id < 0) ? cerr : "";
if (id >= 0 && si == 1) {
// auto-detect tipo via sample (solo para stage 0).
int sample = std::min(64, rows);
std::vector<std::string> samples_str;
std::vector<const char*> samples_ptr;
std::vector<std::string> hn_storage = headers;
std::unordered_map<std::string, int> n2c;
for (int c = 0; c < orig_cols && c < (int)hn_storage.size(); ++c) {
n2c[hn_storage[c]] = c;
}
for (int r = 0; r < sample; ++r) {
lua_engine::RowCtx ctx;
ctx.cells = cells;
ctx.orig_cols = orig_cols;
ctx.row = r;
ctx.header_names = &hn_storage;
ctx.name_to_col = &n2c;
std::string e;
samples_str.emplace_back(
lua_engine::eval(lua_engine::get(), id, ctx, &e));
}
for (auto& s : samples_str) samples_ptr.push_back(s.c_str());
d.type = auto_detect_type(samples_ptr.data(),
(int)samples_ptr.size(), 1, 0);
} else {
d.type = ColumnType::String;
}
stg.derived.push_back(d);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// filter
lua_getfield(L, -1, "filter");
if (lua_istable(L, -1)) {
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (lua_istable(L, -1) && lua_rawlen(L, -1) >= 3) {
lua_rawgeti(L, -1, 1); std::string op = lua_to_string(L, -1); lua_pop(L, 1);
lua_rawgeti(L, -1, 2); std::string col_name = lua_to_string(L, -1); lua_pop(L, 1);
lua_rawgeti(L, -1, 3); std::string val = lua_to_string(L, -1); lua_pop(L, 1);
int ci = find_orig_col(cur_headers, col_name);
if (ci >= 0) {
stg.filters.push_back({ci, parse_op(op), val});
} else {
warn("stage " + std::to_string(si - 1) + ": filter col \"" + col_name + "\" not found");
}
if (op != "=" && op != "!=" && op != ">" && op != ">=" &&
op != "<" && op != "<=" && op != "contains" &&
op != "!contains" && op != "starts" && op != "ends") {
warn("stage " + std::to_string(si - 1) + ": unknown filter op \"" + op + "\" (defaulting to =)");
}
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// breakout (solo aplica stages >= 1, no-op silencioso si stage 0).
// Acepta sufijo ":granularity" para cols Date (fase 10).
lua_getfield(L, -1, "breakout");
if (lua_istable(L, -1)) {
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (lua_isstring(L, -1)) {
std::string bn = lua_tostring(L, -1);
std::string clean;
parse_breakout_granularity(bn, clean);
if (find_orig_col(cur_headers, clean) < 0) {
warn("stage " + std::to_string(si - 1) + ": breakout col \"" + clean + "\" not in input headers");
}
stg.breakouts.emplace_back(bn);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// aggregation
lua_getfield(L, -1, "aggregation");
if (lua_istable(L, -1)) {
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (lua_istable(L, -1) && lua_rawlen(L, -1) >= 1) {
Aggregation a;
lua_rawgeti(L, -1, 1);
std::string fn_name = lua_to_string(L, -1);
lua_pop(L, 1);
bool known = (fn_name == "count" || fn_name == "sum" || fn_name == "avg" ||
fn_name == "min" || fn_name == "max" || fn_name == "distinct" ||
fn_name == "stddev"|| fn_name == "median" ||
fn_name == "p25" || fn_name == "p75" || fn_name == "p90" ||
fn_name == "p99" || fn_name == "percentile");
if (!known) {
warn("stage " + std::to_string(si - 1) + ": unknown aggregation fn \"" + fn_name + "\" (defaulting to count)");
}
a.fn = agg_fn_from_string(fn_name);
if (lua_rawlen(L, -1) >= 2) {
lua_rawgeti(L, -1, 2);
a.col = lua_to_string(L, -1);
lua_pop(L, 1);
if (a.fn != AggFn::Count && find_orig_col(cur_headers, a.col) < 0) {
warn("stage " + std::to_string(si - 1) + ": aggregation col \"" + a.col + "\" not in input headers");
}
} else if (a.fn != AggFn::Count) {
warn("stage " + std::to_string(si - 1) + ": aggregation \"" + fn_name + "\" requires a column");
}
if (lua_rawlen(L, -1) >= 3) {
lua_rawgeti(L, -1, 3);
if (lua_isnumber(L, -1)) a.arg = lua_tonumber(L, -1);
lua_pop(L, 1);
}
stg.aggregations.push_back(a);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// sort
lua_getfield(L, -1, "sort");
if (lua_istable(L, -1)) {
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (lua_istable(L, -1) && lua_rawlen(L, -1) >= 2) {
lua_rawgeti(L, -1, 1); std::string dir = lua_to_string(L, -1); lua_pop(L, 1);
lua_rawgeti(L, -1, 2); std::string col = lua_to_string(L, -1); lua_pop(L, 1);
SortClause sc;
sc.col = col;
sc.desc = (dir == "desc");
if (dir != "asc" && dir != "desc") {
warn("stage " + std::to_string(si - 1) + ": unknown sort dir \"" + dir + "\" (defaulting to asc)");
}
stg.sorts.push_back(sc);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
state.stages.push_back(std::move(stg));
// Advance cur_headers para resolver filter/sort col del siguiente stage.
const Stage& last = state.stages.back();
if (si == 1) {
// Stage 0: cur_headers = orig + derived (sin breakouts/agg).
for (const auto& d : last.derived) cur_headers.push_back(d.name);
} else {
if (!last.breakouts.empty() || !last.aggregations.empty()) {
std::vector<std::string> next;
for (const auto& b : last.breakouts) next.push_back(b);
for (const auto& a : last.aggregations) next.push_back(aggregation_alias(a));
cur_headers = std::move(next);
}
}
lua_pop(L, 1); // pop stage entry
}
}
lua_pop(L, 1); // stages
state.ensure_stage0();
// ---- Walk columns (per-col render config) ----
int eff_cols = orig_cols + (int)state.raw().derived.size();
lua_getfield(L, -1, "columns");
if (lua_istable(L, -1)) {
state.col_visible.assign(eff_cols, true);
std::vector<std::pair<int,int>> order_pairs;
std::vector<bool> seen(eff_cols, false);
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }
lua_getfield(L, -1, "name");
std::string nm = lua_to_string(L, -1);
lua_pop(L, 1);
int col_idx = find_orig_col(headers, nm);
if (col_idx < 0) {
int di = find_derived_idx(state.raw().derived, nm);
if (di >= 0) col_idx = orig_cols + di;
}
if (col_idx < 0 || col_idx >= eff_cols) { lua_pop(L, 1); continue; }
seen[col_idx] = true;
// visible
lua_getfield(L, -1, "visible");
if (lua_isboolean(L, -1)) state.col_visible[col_idx] = lua_toboolean(L, -1);
lua_pop(L, 1);
// order
lua_getfield(L, -1, "order");
int order_val = lua_isnumber(L, -1) ? (int)lua_tointeger(L, -1) : (col_idx + 1);
lua_pop(L, 1);
order_pairs.emplace_back(order_val, col_idx);
// type (mutable solo para derived)
lua_getfield(L, -1, "type");
if (lua_isstring(L, -1)) {
std::string tn = lua_tostring(L, -1);
ColumnType t = column_type_from_string(tn);
if (col_idx >= orig_cols) {
state.raw().derived[col_idx - orig_cols].type = t;
}
}
lua_pop(L, 1);
// color_rules
lua_getfield(L, -1, "color_rules");
if (lua_istable(L, -1)) {
int rn = (int)lua_rawlen(L, -1);
for (int j = 1; j <= rn; ++j) {
lua_rawgeti(L, -1, j);
if (lua_istable(L, -1)) {
lua_getfield(L, -1, "equals");
std::string eq = lua_to_string(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "color");
std::string hx = lua_to_string(L, -1);
lua_pop(L, 1);
state.color_rules.push_back({col_idx, eq, hex_to_color(hx)});
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
lua_pop(L, 1); // pop entry
}
std::sort(order_pairs.begin(), order_pairs.end());
state.col_order.clear();
for (auto& p : order_pairs) state.col_order.push_back(p.second);
for (int c = 0; c < eff_cols; ++c) if (!seen[c]) state.col_order.push_back(c);
}
lua_pop(L, 1); // columns
// ---- Walk views[] (extra viz panels) ----
state.extra_panels.clear();
lua_getfield(L, -1, "views");
if (lua_istable(L, -1)) {
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }
VizPanel p;
lua_getfield(L, -1, "display");
if (lua_isstring(L, -1)) p.display = view_mode_from_token(lua_tostring(L, -1));
lua_pop(L, 1);
auto read_str = [&](const char* key, std::string& out_s) {
lua_getfield(L, -1, key);
if (lua_isstring(L, -1)) out_s = lua_tostring(L, -1);
lua_pop(L, 1);
};
read_str("x_col", p.config.x_col);
read_str("cat_col", p.config.cat_col);
read_str("size_col", p.config.size_col);
lua_getfield(L, -1, "y_cols");
if (lua_istable(L, -1)) {
int yn = (int)lua_rawlen(L, -1);
for (int j = 1; j <= yn; ++j) {
lua_rawgeti(L, -1, j);
if (lua_isstring(L, -1)) p.config.y_cols.emplace_back(lua_tostring(L, -1));
lua_pop(L, 1);
}
}
lua_pop(L, 1);
lua_getfield(L, -1, "color");
if (lua_isstring(L, -1)) p.config.primary_color = hex_to_color(lua_tostring(L, -1));
lua_pop(L, 1);
lua_getfield(L, -1, "hist_bins");
if (lua_isnumber(L, -1)) p.config.hist_bins = (int)lua_tointeger(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "pie_radius");
if (lua_isnumber(L, -1)) p.config.pie_radius = (float)lua_tonumber(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "show_legend");
if (lua_isboolean(L, -1)) p.config.show_legend = lua_toboolean(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "show_markers");
if (lua_isboolean(L, -1)) p.config.show_markers = lua_toboolean(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "locked");
if (lua_isboolean(L, -1)) p.config.locked = lua_toboolean(L, -1);
lua_pop(L, 1);
// Panel 0 = main viz (state.display + state.viz_config).
if (i == 1) {
state.display = p.display;
state.viz_config = p.config;
} else {
state.extra_panels.push_back(p);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1); // views
lua_pop(L, 1); // pop root
return finish_with_warns(true);
}
} // namespace tql
@@ -1,42 +0,0 @@
// TQL — Table Query Language emit/apply. Round-trip entre State y Lua text.
// Ver docs/TQL.md.
#pragma once
#include "data_table_logic.h"
#include <string>
#include <vector>
namespace tql {
// Serializa el estado actual a un Lua chunk completo:
// return { version, display, stages, columns, visualization_settings }
//
// `headers` y `types` describen las cols originales (size = orig_cols).
// Las derived cols se anaden automaticamente desde state.derived.
std::string emit(const data_table::State& state,
const std::vector<std::string>& headers,
const std::vector<data_table::ColumnType>& types);
// Parsea un Lua chunk TQL y rellena State. Mutates:
// - stages (clears + reconstruye desde stages[] del TQL; stage 0 = Raw con
// filters/expressions/sort; stages 1+ con filter/breakout/aggregation/sort)
// - col_visible / col_order (desde columns[])
// - color_rules (desde columns[].color_rules)
// - stages[0].derived[].type (desde columns[].type para nombres derived)
//
// `cells/rows/orig_cols` necesarios para sample auto-detect de tipos en
// expressions (cuando la entry columns omite el type explicito).
bool apply(const std::string& lua_text,
data_table::State& state,
const std::vector<std::string>& headers,
const std::vector<data_table::ColumnType>& types,
const char* const* cells, int rows, int orig_cols,
std::string* err);
// Helpers expuestos para tests.
std::string lua_string_literal(const std::string& s);
std::string color_to_hex(unsigned int c);
unsigned int hex_to_color(const std::string& s);
data_table::ColumnType column_type_from_string(const std::string& s);
} // namespace tql
@@ -1,231 +0,0 @@
// tql_duckdb.cpp — DuckDB adapter para ejecutar SQL emitido por tql_to_sql.
// Compilado solo si FN_TQL_DUCKDB define. Ver issue 0080.
#include "tql_duckdb.h"
#ifdef FN_TQL_DUCKDB
#include "duckdb.h"
#include <chrono>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
namespace tql_duckdb {
using namespace data_table;
namespace {
// SQL identifier quote (mismo patron que tql_to_sql).
std::string sql_ident(const std::string& name) {
std::string out;
out.reserve(name.size() + 4);
out += '"';
for (char c : name) {
if (c == '"') out += "\"\"";
else out += c;
}
out += '"';
return out;
}
const char* duckdb_type_for(ColumnType t) {
switch (t) {
case ColumnType::Int: return "BIGINT";
case ColumnType::Float: return "DOUBLE";
case ColumnType::Bool: return "BOOLEAN";
case ColumnType::Date: return "VARCHAR"; // se almacena ISO como texto v1
case ColumnType::Json: return "VARCHAR";
default: return "VARCHAR";
}
}
// SQL literal escape para string.
std::string lit_str(const char* s) {
std::string out = "'";
for (const char* p = s ? s : ""; *p; ++p) {
if (*p == '\'') out += "''";
else out += *p;
}
out += "'";
return out;
}
bool create_and_load(duckdb_connection cn, const TableInput& t, std::string& err) {
// CREATE TABLE
std::string ddl = "CREATE TABLE " + sql_ident(t.name) + " (";
for (size_t i = 0; i < t.headers.size(); ++i) {
if (i > 0) ddl += ", ";
ColumnType ct = (i < t.types.size()) ? t.types[i] : ColumnType::String;
ddl += sql_ident(t.headers[i]) + " " + duckdb_type_for(ct);
}
ddl += ");";
duckdb_result rr;
if (duckdb_query(cn, ddl.c_str(), &rr) == DuckDBError) {
err = duckdb_result_error(&rr);
duckdb_destroy_result(&rr);
return false;
}
duckdb_destroy_result(&rr);
// INSERT rows via VALUES batches (1000 rows/insert).
if (t.rows == 0 || t.cols == 0) return true;
const int batch = 1000;
for (int r0 = 0; r0 < t.rows; r0 += batch) {
int r1 = (r0 + batch < t.rows) ? r0 + batch : t.rows;
std::string ins = "INSERT INTO " + sql_ident(t.name) + " VALUES ";
for (int r = r0; r < r1; ++r) {
if (r > r0) ins += ", ";
ins += "(";
for (int c = 0; c < t.cols; ++c) {
if (c > 0) ins += ", ";
const char* v = t.cells[r * t.cols + c];
if (!v || !*v) { ins += "NULL"; continue; }
ColumnType ct = (c < (int)t.types.size()) ? t.types[c] : ColumnType::String;
if (ct == ColumnType::Int || ct == ColumnType::Float) {
// Asumir parseable; sino DuckDB error.
ins += v;
} else if (ct == ColumnType::Bool) {
ins += (std::strcmp(v, "true") == 0) ? "TRUE" : "FALSE";
} else {
ins += lit_str(v);
}
}
ins += ")";
}
ins += ";";
if (duckdb_query(cn, ins.c_str(), &rr) == DuckDBError) {
err = std::string("INSERT into ") + t.name + ": " + duckdb_result_error(&rr);
duckdb_destroy_result(&rr);
return false;
}
duckdb_destroy_result(&rr);
}
return true;
}
ColumnType type_from_duckdb(duckdb_type t) {
switch (t) {
case DUCKDB_TYPE_BOOLEAN: return ColumnType::Bool;
case DUCKDB_TYPE_TINYINT:
case DUCKDB_TYPE_SMALLINT:
case DUCKDB_TYPE_INTEGER:
case DUCKDB_TYPE_BIGINT:
case DUCKDB_TYPE_HUGEINT:
case DUCKDB_TYPE_UTINYINT:
case DUCKDB_TYPE_USMALLINT:
case DUCKDB_TYPE_UINTEGER:
case DUCKDB_TYPE_UBIGINT:
return ColumnType::Int;
case DUCKDB_TYPE_FLOAT:
case DUCKDB_TYPE_DOUBLE:
case DUCKDB_TYPE_DECIMAL:
return ColumnType::Float;
case DUCKDB_TYPE_DATE:
case DUCKDB_TYPE_TIMESTAMP:
return ColumnType::Date;
default:
return ColumnType::String;
}
}
} // anon
Result execute(const std::string& sql,
const std::vector<std::string>& params,
const std::vector<TableInput>& tables) {
Result out;
auto t0 = std::chrono::steady_clock::now();
duckdb_database db = nullptr;
duckdb_connection cn = nullptr;
if (duckdb_open(nullptr, &db) == DuckDBError) {
out.error = "duckdb_open failed";
return out;
}
if (duckdb_connect(db, &cn) == DuckDBError) {
out.error = "duckdb_connect failed";
duckdb_close(&db);
return out;
}
// Crear y poblar tablas.
for (const auto& t : tables) {
std::string e;
if (!create_and_load(cn, t, e)) {
out.error = e;
duckdb_disconnect(&cn);
duckdb_close(&db);
return out;
}
}
// Preparar + bind params.
duckdb_prepared_statement stmt = nullptr;
if (duckdb_prepare(cn, sql.c_str(), &stmt) == DuckDBError) {
out.error = std::string("prepare: ") + duckdb_prepare_error(stmt);
duckdb_destroy_prepare(&stmt);
duckdb_disconnect(&cn);
duckdb_close(&db);
return out;
}
for (size_t i = 0; i < params.size(); ++i) {
// DuckDB params son 1-based.
if (duckdb_bind_varchar(stmt, (idx_t)(i + 1), params[i].c_str()) == DuckDBError) {
out.error = "bind param fail";
duckdb_destroy_prepare(&stmt);
duckdb_disconnect(&cn);
duckdb_close(&db);
return out;
}
}
duckdb_result res;
if (duckdb_execute_prepared(stmt, &res) == DuckDBError) {
out.error = std::string("execute: ") + duckdb_result_error(&res);
duckdb_destroy_result(&res);
duckdb_destroy_prepare(&stmt);
duckdb_disconnect(&cn);
duckdb_close(&db);
return out;
}
// Materializar resultado en StageOutput.
idx_t cols = duckdb_column_count(&res);
idx_t rows = duckdb_row_count(&res);
out.out.cols = (int)cols;
out.out.rows = (int)rows;
out.row_count = (int)rows;
out.out.headers.reserve(cols);
out.out.types.reserve(cols);
for (idx_t c = 0; c < cols; ++c) {
out.out.headers.emplace_back(duckdb_column_name(&res, c));
out.out.types.push_back(type_from_duckdb(duckdb_column_type(&res, c)));
}
out.out.cell_backing.reserve(rows * cols);
out.out.cells.reserve(rows * cols);
for (idx_t r = 0; r < rows; ++r) {
for (idx_t c = 0; c < cols; ++c) {
char* v = duckdb_value_varchar(&res, c, r);
out.out.cell_backing.emplace_back(v ? v : "");
if (v) duckdb_free(v);
}
}
for (auto& s : out.out.cell_backing) out.out.cells.push_back(s.c_str());
duckdb_destroy_result(&res);
duckdb_destroy_prepare(&stmt);
duckdb_disconnect(&cn);
duckdb_close(&db);
auto t1 = std::chrono::steady_clock::now();
out.duration_ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
return out;
}
} // namespace tql_duckdb
#endif // FN_TQL_DUCKDB
@@ -1,29 +0,0 @@
// tql_duckdb: ejecuta SQL DuckDB sobre TableInputs in-memory.
// Solo se compila si FN_TQL_DUCKDB esta definido. Adapter opcional para
// tql_to_sql emit -> execute. Ver issue 0080.
#pragma once
#ifdef FN_TQL_DUCKDB
#include "data_table_logic.h"
#include <string>
#include <vector>
namespace tql_duckdb {
struct Result {
data_table::StageOutput out;
std::string error; // non-empty si fallo
int row_count = 0;
double duration_ms = 0.0;
};
// Impure: abre DuckDB in-memory, registra tablas como CREATE TABLE + INSERT,
// prepara sql con `?` placeholders bound a `params`, materializa resultado.
Result execute(const std::string& sql,
const std::vector<std::string>& params,
const std::vector<data_table::TableInput>& tables);
} // namespace tql_duckdb
#endif // FN_TQL_DUCKDB
@@ -1,862 +0,0 @@
// tql_to_sql.cpp — pure walker TQL -> SQL DuckDB + Lua subset transpiler.
// Ver issue 0080. Sin DuckDB linkado.
#include "tql_to_sql.h"
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <set>
#include <sstream>
#include <unordered_map>
namespace tql_to_sql {
using namespace data_table;
// ============================================================================
// Lua subset tokenizer + recursive-descent expression parser -> SQL string.
// ============================================================================
namespace {
struct Tok {
enum Kind {
EndT, NumT, StrT, IdentT, ColT,
// operators / keywords
Plus, Minus, Star, Slash, Percent, ConcatT,
Eq, Neq, Lt, Lte, Gt, Gte,
AndT, OrT, NotT,
IfT, ThenT, ElseT, EndKW,
LParen, RParen, Comma, Dot,
TrueT, FalseT, NilT,
} kind = EndT;
std::string text; // raw token texto (para idents/numbers/strings)
};
// Categorias prohibidas: token literal -> mensaje.
const std::unordered_map<std::string, const char*>& forbidden_keywords() {
static const std::unordered_map<std::string, const char*> M = {
{"function", "closures not allowed in SQL transpile subset"},
{"local", "local declarations not allowed"},
{"for", "loops not allowed"},
{"while", "loops not allowed"},
{"repeat", "loops not allowed"},
{"do", "block statements not allowed"},
{"return", "explicit return not allowed (formula is implicit expression)"},
{"goto", "goto not allowed"},
{"break", "break not allowed (no loops)"},
// io/os/debug/coroutines
{"io", "io.* access not allowed"},
{"os", "os.* access not allowed"},
{"debug", "debug.* access not allowed"},
{"package", "package access not allowed"},
{"require", "require not allowed"},
{"coroutine","coroutines not allowed"},
{"setmetatable","metatables not allowed"},
{"getmetatable","metatables not allowed"},
{"rawget", "rawget not allowed"},
{"rawset", "rawset not allowed"},
{"pcall", "pcall not allowed"},
{"xpcall", "xpcall not allowed"},
{"print", "print not allowed (SQL has no side effects)"},
};
return M;
}
// Whitelist de funciones SQL-transpilables: lua name -> SQL function template.
// Template usa $1, $2, ... como placeholders de argumentos.
struct FnMap { int min_args; int max_args; const char* sql_tmpl; };
const std::unordered_map<std::string, FnMap>& fn_whitelist() {
static const std::unordered_map<std::string, FnMap> M = {
// math.*
{"math.floor", {1, 1, "floor($1)"}},
{"math.ceil", {1, 1, "ceiling($1)"}},
{"math.abs", {1, 1, "abs($1)"}},
{"math.sqrt", {1, 1, "sqrt($1)"}},
{"math.sin", {1, 1, "sin($1)"}},
{"math.cos", {1, 1, "cos($1)"}},
{"math.log", {1, 1, "ln($1)"}},
{"math.exp", {1, 1, "exp($1)"}},
{"math.min", {2, 2, "least($1, $2)"}},
{"math.max", {2, 2, "greatest($1, $2)"}},
// string.*
{"string.upper", {1, 1, "upper($1)"}},
{"string.lower", {1, 1, "lower($1)"}},
{"string.len", {1, 1, "length($1)"}},
{"string.sub", {2, 3, "/*SUBSTRING*/"}}, // manejo especial: argc 2 vs 3
// top-level
{"tostring", {1, 1, "CAST($1 AS VARCHAR)"}},
{"tonumber", {1, 1, "CAST($1 AS DOUBLE)"}},
};
return M;
}
// Identifier SQL-safe: si tiene caracteres especiales o coincide con keyword,
// usar `"col"`. Aqui simplificado: siempre quote con dobles comillas para
// preservar case y permitir `:` (sufijo granularity).
std::string sql_ident(const std::string& name) {
std::string out;
out.reserve(name.size() + 4);
out += '"';
for (char c : name) {
if (c == '"') out += "\"\""; // escape
else out += c;
}
out += '"';
return out;
}
std::string sql_string_literal(const std::string& s) {
std::string out;
out.reserve(s.size() + 4);
out += '\'';
for (char c : s) {
if (c == '\'') out += "''";
else out += c;
}
out += '\'';
return out;
}
class Lexer {
public:
Lexer(const std::string& src) : src_(src) {}
// Devuelve true si parsea OK. False con err en error_.
bool tokenize(std::vector<Tok>& out) {
size_t i = 0;
while (i < src_.size()) {
char c = src_[i];
if (std::isspace((unsigned char)c)) { ++i; continue; }
// Lua line comment
if (c == '-' && i + 1 < src_.size() && src_[i+1] == '-') {
while (i < src_.size() && src_[i] != '\n') ++i;
continue;
}
if (c == '[' ) {
// col ref [identifier]
size_t j = i + 1;
std::string name;
while (j < src_.size() && src_[j] != ']') {
name += src_[j];
++j;
}
if (j >= src_.size()) { error_ = "unterminated [col] ref"; return false; }
Tok t; t.kind = Tok::ColT; t.text = name;
out.push_back(t);
i = j + 1;
continue;
}
if (c == '"' || c == '\'') {
char q = c;
++i;
std::string s;
while (i < src_.size() && src_[i] != q) {
if (src_[i] == '\\' && i + 1 < src_.size()) {
char esc = src_[i+1];
if (esc == 'n') s += '\n';
else if (esc == 't') s += '\t';
else if (esc == '\\') s += '\\';
else if (esc == '\'') s += '\'';
else if (esc == '"') s += '"';
else s += esc;
i += 2;
} else {
s += src_[i++];
}
}
if (i >= src_.size()) { error_ = "unterminated string literal"; return false; }
++i;
Tok t; t.kind = Tok::StrT; t.text = s;
out.push_back(t);
continue;
}
if (std::isdigit((unsigned char)c) || (c == '.' && i + 1 < src_.size() && std::isdigit((unsigned char)src_[i+1]))) {
std::string n;
bool seen_dot = false;
while (i < src_.size()) {
char d = src_[i];
if (std::isdigit((unsigned char)d)) { n += d; ++i; }
else if (d == '.' && !seen_dot) { n += d; seen_dot = true; ++i; }
else break;
}
Tok t; t.kind = Tok::NumT; t.text = n;
out.push_back(t);
continue;
}
if (std::isalpha((unsigned char)c) || c == '_') {
std::string id;
while (i < src_.size() &&
(std::isalnum((unsigned char)src_[i]) || src_[i] == '_')) {
id += src_[i++];
}
// Check forbidden keywords y mapeo a tokens.
auto& F = forbidden_keywords();
auto fit = F.find(id);
if (fit != F.end()) {
error_ = std::string("token '") + id + "': " + fit->second;
return false;
}
Tok t;
if (id == "and") t.kind = Tok::AndT;
else if (id == "or") t.kind = Tok::OrT;
else if (id == "not") t.kind = Tok::NotT;
else if (id == "if") t.kind = Tok::IfT;
else if (id == "then") t.kind = Tok::ThenT;
else if (id == "else") t.kind = Tok::ElseT;
else if (id == "end") t.kind = Tok::EndKW;
else if (id == "true") t.kind = Tok::TrueT;
else if (id == "false") t.kind = Tok::FalseT;
else if (id == "nil") t.kind = Tok::NilT;
else { t.kind = Tok::IdentT; t.text = id; }
out.push_back(t);
continue;
}
// Operators
auto emit = [&](Tok::Kind k, int len) {
Tok t; t.kind = k; out.push_back(t); i += (size_t)len;
};
if (c == '+') { emit(Tok::Plus, 1); continue; }
if (c == '-') { emit(Tok::Minus, 1); continue; }
if (c == '*') { emit(Tok::Star, 1); continue; }
if (c == '/') { emit(Tok::Slash, 1); continue; }
if (c == '%') { emit(Tok::Percent,1); continue; }
if (c == '(') { emit(Tok::LParen, 1); continue; }
if (c == ')') { emit(Tok::RParen, 1); continue; }
if (c == ',') { emit(Tok::Comma, 1); continue; }
if (c == '.') {
if (i + 1 < src_.size() && src_[i+1] == '.') {
if (i + 2 < src_.size() && src_[i+2] == '.') {
error_ = "'...' vararg not allowed"; return false;
}
emit(Tok::ConcatT, 2); continue;
}
emit(Tok::Dot, 1); continue;
}
if (c == '=') {
if (i + 1 < src_.size() && src_[i+1] == '=') { emit(Tok::Eq, 2); continue; }
error_ = "single '=' (assignment) not allowed"; return false;
}
if (c == '~') {
if (i + 1 < src_.size() && src_[i+1] == '=') { emit(Tok::Neq, 2); continue; }
error_ = "stray '~'"; return false;
}
if (c == '<') {
if (i + 1 < src_.size() && src_[i+1] == '=') { emit(Tok::Lte, 2); continue; }
emit(Tok::Lt, 1); continue;
}
if (c == '>') {
if (i + 1 < src_.size() && src_[i+1] == '=') { emit(Tok::Gte, 2); continue; }
emit(Tok::Gt, 1); continue;
}
if (c == '{') { error_ = "table literals '{...}' not allowed"; return false; }
if (c == '}') { error_ = "stray '}'"; return false; }
if (c == ';') { error_ = "multi-statement not allowed"; return false; }
if (c == '#') { error_ = "length '#' operator not allowed"; return false; }
if (c == ':') { error_ = "method calls ':' not allowed"; return false; }
error_ = std::string("unexpected character '") + c + "'";
return false;
}
Tok t; t.kind = Tok::EndT;
out.push_back(t);
return true;
}
const std::string& error() const { return error_; }
private:
const std::string& src_;
std::string error_;
};
class Parser {
public:
Parser(const std::vector<Tok>& toks,
const std::vector<std::string>& headers)
: toks_(toks), headers_(headers) {}
// expr := ternary
// ternary := if/then/else | logic_or
bool parse_expr(std::string& out) {
return parse_ternary(out);
}
bool parse_ternary(std::string& out) {
if (peek(0).kind == Tok::IfT) {
++pos_;
std::string a, b, c;
if (!parse_logic_or(a)) return false;
if (!eat(Tok::ThenT, "'then' expected after 'if'")) return false;
if (!parse_ternary(b)) return false;
if (!eat(Tok::ElseT, "'else' expected (subset requires else branch)")) return false;
if (!parse_ternary(c)) return false;
if (!eat(Tok::EndKW, "'end' expected to close 'if'")) return false;
out = "CASE WHEN " + a + " THEN " + b + " ELSE " + c + " END";
return true;
}
return parse_logic_or(out);
}
bool parse_logic_or(std::string& out) {
if (!parse_logic_and(out)) return false;
while (peek(0).kind == Tok::OrT) {
++pos_;
std::string rhs;
if (!parse_logic_and(rhs)) return false;
out = "(" + out + " OR " + rhs + ")";
}
return true;
}
bool parse_logic_and(std::string& out) {
if (!parse_not(out)) return false;
while (peek(0).kind == Tok::AndT) {
++pos_;
std::string rhs;
if (!parse_not(rhs)) return false;
out = "(" + out + " AND " + rhs + ")";
}
return true;
}
bool parse_not(std::string& out) {
if (peek(0).kind == Tok::NotT) {
++pos_;
std::string e;
if (!parse_not(e)) return false;
out = "NOT (" + e + ")";
return true;
}
return parse_comparison(out);
}
bool parse_comparison(std::string& out) {
if (!parse_concat(out)) return false;
while (true) {
Tok::Kind k = peek(0).kind;
const char* op = nullptr;
if (k == Tok::Eq) op = " = ";
else if (k == Tok::Neq) op = " <> ";
else if (k == Tok::Lt) op = " < ";
else if (k == Tok::Lte) op = " <= ";
else if (k == Tok::Gt) op = " > ";
else if (k == Tok::Gte) op = " >= ";
else break;
++pos_;
std::string rhs;
if (!parse_concat(rhs)) return false;
out = "(" + out + op + rhs + ")";
}
return true;
}
bool parse_concat(std::string& out) {
if (!parse_additive(out)) return false;
while (peek(0).kind == Tok::ConcatT) {
++pos_;
std::string rhs;
if (!parse_additive(rhs)) return false;
out = "(" + out + " || " + rhs + ")";
}
return true;
}
bool parse_additive(std::string& out) {
if (!parse_multiplicative(out)) return false;
while (peek(0).kind == Tok::Plus || peek(0).kind == Tok::Minus) {
const char* op = (peek(0).kind == Tok::Plus) ? " + " : " - ";
++pos_;
std::string rhs;
if (!parse_multiplicative(rhs)) return false;
out = "(" + out + op + rhs + ")";
}
return true;
}
bool parse_multiplicative(std::string& out) {
if (!parse_unary(out)) return false;
while (peek(0).kind == Tok::Star || peek(0).kind == Tok::Slash || peek(0).kind == Tok::Percent) {
const char* op = (peek(0).kind == Tok::Star) ? " * "
: (peek(0).kind == Tok::Slash) ? " / " : " % ";
++pos_;
std::string rhs;
if (!parse_unary(rhs)) return false;
out = "(" + out + op + rhs + ")";
}
return true;
}
bool parse_unary(std::string& out) {
if (peek(0).kind == Tok::Minus) {
++pos_;
std::string e;
if (!parse_unary(e)) return false;
out = "(-" + e + ")";
return true;
}
return parse_primary(out);
}
bool parse_primary(std::string& out) {
Tok t = peek(0);
if (t.kind == Tok::NumT) {
++pos_;
out = t.text;
return true;
}
if (t.kind == Tok::StrT) {
++pos_;
out = sql_string_literal(t.text);
return true;
}
if (t.kind == Tok::TrueT) { ++pos_; out = "TRUE"; return true; }
if (t.kind == Tok::FalseT) { ++pos_; out = "FALSE"; return true; }
if (t.kind == Tok::NilT) { ++pos_; out = "NULL"; return true; }
if (t.kind == Tok::ColT) {
// Check col exists (warning, not error).
++pos_;
(void)headers_; // currently not validating — caller can do that
out = sql_ident(t.text);
return true;
}
if (t.kind == Tok::LParen) {
++pos_;
std::string e;
if (!parse_expr(e)) return false;
if (!eat(Tok::RParen, "expected ')'")) return false;
out = "(" + e + ")";
return true;
}
if (t.kind == Tok::IdentT) {
// Function call: identifier ("." identifier)? "(" args ")"
std::string name = t.text;
++pos_;
if (peek(0).kind == Tok::Dot) {
++pos_;
if (peek(0).kind != Tok::IdentT) {
error_ = "expected identifier after '.'";
return false;
}
name += "." + peek(0).text;
++pos_;
}
if (peek(0).kind != Tok::LParen) {
error_ = "bare identifier '" + name +
"' not allowed (only [col] refs + whitelisted fn calls)";
return false;
}
++pos_; // consume '('
std::vector<std::string> args;
if (peek(0).kind != Tok::RParen) {
while (true) {
std::string a;
if (!parse_expr(a)) return false;
args.push_back(a);
if (peek(0).kind == Tok::Comma) { ++pos_; continue; }
break;
}
}
if (!eat(Tok::RParen, "expected ')' closing function args")) return false;
// Validate against whitelist
auto& W = fn_whitelist();
auto wit = W.find(name);
if (wit == W.end()) {
error_ = "function '" + name +
"' not in SQL transpile whitelist (math.*, string.upper/lower/len/sub, tostring, tonumber)";
return false;
}
const FnMap& fm = wit->second;
if ((int)args.size() < fm.min_args || (int)args.size() > fm.max_args) {
std::ostringstream os;
os << "function '" << name << "' takes " << fm.min_args;
if (fm.max_args != fm.min_args) os << ".." << fm.max_args;
os << " args, got " << args.size();
error_ = os.str();
return false;
}
// Casos especiales
if (name == "string.sub") {
// Lua: string.sub(s, i [, j]) — i/j 1-based, inclusive.
// SQL DuckDB: substring(s, i, count). count = j - i + 1.
if (args.size() == 2) {
// sin j -> hasta el final. DuckDB substring(s, i) acepta.
out = "substring(" + args[0] + ", " + args[1] + ")";
} else {
out = "substring(" + args[0] + ", " + args[1] +
", (" + args[2] + ") - (" + args[1] + ") + 1)";
}
return true;
}
// Generico: substituir $1..$N en template.
std::string s = fm.sql_tmpl;
for (int i = 0; i < (int)args.size(); ++i) {
char ph[6];
std::snprintf(ph, sizeof(ph), "$%d", i + 1);
std::string p = ph;
size_t at = 0;
while ((at = s.find(p, at)) != std::string::npos) {
s.replace(at, p.size(), args[i]);
at += args[i].size();
}
}
out = s;
return true;
}
error_ = std::string("unexpected token in expression");
return false;
}
bool eat(Tok::Kind k, const char* msg) {
if (peek(0).kind != k) { error_ = msg; return false; }
++pos_;
return true;
}
const Tok& peek(int off) const {
size_t i = pos_ + (size_t)off;
if (i >= toks_.size()) return toks_.back();
return toks_[i];
}
bool at_end() const { return peek(0).kind == Tok::EndT; }
const std::string& error() const { return error_; }
private:
const std::vector<Tok>& toks_;
const std::vector<std::string>& headers_;
size_t pos_ = 0;
std::string error_;
};
} // anon
std::string transpile_expr(const std::string& formula,
const std::vector<std::string>& in_headers,
std::string& error_out) {
error_out.clear();
std::vector<Tok> toks;
Lexer lex(formula);
if (!lex.tokenize(toks)) {
error_out = lex.error();
return "";
}
Parser p(toks, in_headers);
std::string out;
if (!p.parse_expr(out)) {
error_out = p.error();
return "";
}
if (!p.at_end()) {
error_out = "unexpected trailing tokens after expression";
return "";
}
return out;
}
bool is_transpilable(const std::string& formula, std::string& error_out) {
std::vector<std::string> empty;
std::string s = transpile_expr(formula, empty, error_out);
return error_out.empty() && !s.empty();
}
// ============================================================================
// TQL State -> SQL DuckDB emitter.
// ============================================================================
namespace {
// Mapeo aggregation -> SQL DuckDB expression.
std::string emit_agg_expr(const Aggregation& a) {
switch (a.fn) {
case AggFn::Count: return "COUNT(*)";
case AggFn::Sum: return "SUM(" + sql_ident(a.col) + ")";
case AggFn::Avg: return "AVG(" + sql_ident(a.col) + ")";
case AggFn::Min: return "MIN(" + sql_ident(a.col) + ")";
case AggFn::Max: return "MAX(" + sql_ident(a.col) + ")";
case AggFn::Distinct: return "COUNT(DISTINCT " + sql_ident(a.col) + ")";
case AggFn::Stddev: return "STDDEV(" + sql_ident(a.col) + ")";
case AggFn::Median: return "quantile_cont(" + sql_ident(a.col) + ", 0.5)";
case AggFn::P25: return "quantile_cont(" + sql_ident(a.col) + ", 0.25)";
case AggFn::P75: return "quantile_cont(" + sql_ident(a.col) + ", 0.75)";
case AggFn::P90: return "quantile_cont(" + sql_ident(a.col) + ", 0.90)";
case AggFn::P99: return "quantile_cont(" + sql_ident(a.col) + ", 0.99)";
case AggFn::Percentile: {
char buf[32];
std::snprintf(buf, sizeof(buf), "%g", a.arg);
return std::string("quantile_cont(") + sql_ident(a.col) + ", " + buf + ")";
}
}
return "/* unknown agg */ NULL";
}
std::string emit_breakout_expr(const std::string& bk) {
std::string col_clean;
DateGranularity g = parse_breakout_granularity(bk, col_clean);
if (g == DateGranularity::None) {
return sql_ident(col_clean);
}
const char* tok = date_granularity_token(g);
// Week: DuckDB date_trunc('week', col) -> monday segun configuracion.
return std::string("date_trunc('") + tok + "', " + sql_ident(col_clean) + ")";
}
// Resuelve un Op a operador SQL + (opcional) override de RHS.
const char* sql_op(Op op) {
switch (op) {
case Op::Eq: return " = ";
case Op::Neq: return " <> ";
case Op::Gt: return " > ";
case Op::Gte: return " >= ";
case Op::Lt: return " < ";
case Op::Lte: return " <= ";
case Op::Contains: return " LIKE ";
case Op::NotContains: return " NOT LIKE ";
case Op::StartsWith: return " LIKE ";
case Op::EndsWith: return " LIKE ";
}
return " = ";
}
// Construye RHS literal/pattern segun op + value. Devuelve placeholder '?'
// y push de params; o pattern string-literal directo para LIKE wildcards.
std::string emit_filter_rhs(const Filter& f, std::vector<std::string>& params) {
if (f.op == Op::Contains || f.op == Op::NotContains) {
std::string v = "%" + f.value + "%";
params.push_back(v);
return "?";
}
if (f.op == Op::StartsWith) {
std::string v = f.value + "%";
params.push_back(v);
return "?";
}
if (f.op == Op::EndsWith) {
std::string v = "%" + f.value;
params.push_back(v);
return "?";
}
params.push_back(f.value);
return "?";
}
// Construye CTE stage 0 (Raw): SELECT cols + derived FROM main_t [JOINs].
// `tables` provee schema. main_t name = tables[main_idx].name. Derived cols
// se transpilan a SQL expression; si fuera de subset, push warning + skip col.
bool emit_stage0(const State& st, const std::vector<TableInput>& tables,
int main_idx, SqlEmit& e) {
if (main_idx < 0 || main_idx >= (int)tables.size()) {
e.error = "main table out of range";
return false;
}
const TableInput& main_t = tables[(size_t)main_idx];
// SELECT list: cols originales + derived expressions (subset).
std::string select_list;
for (size_t i = 0; i < main_t.headers.size(); ++i) {
if (i > 0) select_list += ", ";
select_list += sql_ident(main_t.headers[i]);
}
// Derived cols (stage 0 derived).
if (!st.stages.empty()) {
const Stage& s0 = st.stages[0];
for (const auto& d : s0.derived) {
if (d.source_col >= 0 && d.formula.empty()) {
// Retipo puro: alias col origen.
if (d.source_col < (int)main_t.headers.size()) {
select_list += ", " + sql_ident(main_t.headers[(size_t)d.source_col])
+ " AS " + sql_ident(d.name);
}
continue;
}
std::string err;
std::string expr = transpile_expr(d.formula, main_t.headers, err);
if (!err.empty()) {
std::string msg = "derived col '" + d.name +
"' formula out of SQL subset: " + err;
e.warnings.push_back(msg);
// Skip col en SQL output; agente puede recurrir a TQL puro.
continue;
}
select_list += ", " + expr + " AS " + sql_ident(d.name);
}
}
std::string from = sql_ident(main_t.name);
// Joins
for (const auto& jn : st.joins) {
const TableInput* right = nullptr;
for (const auto& ti : tables) {
if (ti.name == jn.source) { right = &ti; break; }
}
if (!right) {
e.warnings.push_back("join source '" + jn.source + "' not in tables");
continue;
}
const char* strat = "LEFT JOIN";
switch (jn.strategy) {
case JoinStrategy::Left: strat = "LEFT JOIN"; break;
case JoinStrategy::Inner: strat = "INNER JOIN"; break;
case JoinStrategy::Right: strat = "RIGHT JOIN"; break;
case JoinStrategy::Full: strat = "FULL OUTER JOIN"; break;
}
from += "\n " + std::string(strat) + " " + sql_ident(right->name)
+ " AS " + sql_ident(jn.alias) + " ON ";
for (size_t k = 0; k < jn.on.size(); ++k) {
if (k > 0) from += " AND ";
from += sql_ident(main_t.name) + "." + sql_ident(jn.on[k].first)
+ " = " + sql_ident(jn.alias) + "." + sql_ident(jn.on[k].second);
}
// Anadir cols del right al SELECT con alias.col prefix.
if (jn.fields.empty()) {
for (const auto& rh : right->headers) {
std::string aliased = jn.alias + "." + rh;
select_list += ", " + sql_ident(jn.alias) + "." + sql_ident(rh)
+ " AS " + sql_ident(aliased);
}
} else {
for (const auto& fld : jn.fields) {
std::string aliased = jn.alias + "." + fld;
select_list += ", " + sql_ident(jn.alias) + "." + sql_ident(fld)
+ " AS " + sql_ident(aliased);
}
}
}
// Stage 0 WHERE: filters del Raw (filter col idx en eff_headers).
// Filter.col es indice en eff_headers (orig + derived). Para SQL emit,
// necesitamos resolver col idx -> col name. Reconstruir orden eff_headers.
std::vector<std::string> eff_headers = main_t.headers;
if (!st.stages.empty()) {
for (const auto& d : st.stages[0].derived) {
eff_headers.push_back(d.name);
}
}
std::string where_clause;
if (!st.stages.empty()) {
const Stage& s0 = st.stages[0];
for (size_t fi = 0; fi < s0.filters.size(); ++fi) {
const Filter& f = s0.filters[fi];
if (f.col < 0 || f.col >= (int)eff_headers.size()) {
e.warnings.push_back("stage0 filter col idx out of range");
continue;
}
std::string col = sql_ident(eff_headers[(size_t)f.col]);
if (!where_clause.empty()) where_clause += " AND ";
where_clause += col + sql_op(f.op) + emit_filter_rhs(f, e.params);
}
}
// Stage 0 sort
std::string order_clause;
if (!st.stages.empty()) {
const Stage& s0 = st.stages[0];
for (size_t si = 0; si < s0.sorts.size(); ++si) {
const SortClause& sc = s0.sorts[si];
if (!order_clause.empty()) order_clause += ", ";
order_clause += sql_ident(sc.col) + (sc.desc ? " DESC" : " ASC");
}
}
std::string cte = "t0 AS (\n SELECT " + select_list + "\n FROM " + from;
if (!where_clause.empty()) cte += "\n WHERE " + where_clause;
if (!order_clause.empty()) cte += "\n ORDER BY " + order_clause;
cte += "\n)";
e.sql = "WITH " + cte;
return true;
}
// Stage N (N>=1): SELECT breakouts + agg expressions FROM t<N-1>
// [WHERE filters] [GROUP BY ...] [ORDER BY ...].
bool emit_stage_n(const Stage& stg, int n, SqlEmit& e) {
std::string prev = "t" + std::to_string(n - 1);
std::string cur = "t" + std::to_string(n);
// SELECT list: breakouts (con granularity expr si aplica) + aggregations.
std::string select_list;
for (size_t i = 0; i < stg.breakouts.size(); ++i) {
if (i > 0) select_list += ", ";
select_list += emit_breakout_expr(stg.breakouts[i])
+ " AS " + sql_ident(stg.breakouts[i]);
}
for (size_t i = 0; i < stg.aggregations.size(); ++i) {
if (!select_list.empty()) select_list += ", ";
std::string alias = aggregation_alias(stg.aggregations[i]);
select_list += emit_agg_expr(stg.aggregations[i]) + " AS " + sql_ident(alias);
}
if (select_list.empty()) select_list = "*";
// WHERE: filters del stage. col es indice en input headers (output del stage previo).
// Aproximacion: usamos el nombre via stage breakouts/aggs del stage previo si fuera necesario.
// Para v1, emit por nombre cuando filter.col >= 0 sea idx en breakouts/aggs/orig. El
// chequeo de existencia se delega a DuckDB (errores en execute son detectables).
// V1 simple: skip filter cuando no podemos resolver — caller solo deberia tener filter
// sobre cols que existen.
// Estrategia simple: emite WHERE solo si stage previo provee headers conocidos. Para no
// duplicar logica, dejamos al caller proveer headers via filter.col que se resuelve a
// breakouts[col].
// V1: si filter.col esta en rango de breakouts del stage previo, emite breakout name.
// Sino, warning + skip.
std::string where_clause;
// Best effort: no podemos construir headers del stage previo aqui sin recomputar.
// Para v1, omitimos filters de stages >=1 — caller deberia evitar usarlos via SQL.
// TODO v2: pasar prev_headers para resolver.
(void)where_clause;
// GROUP BY: solo si hay breakouts.
std::string group_clause;
for (size_t i = 0; i < stg.breakouts.size(); ++i) {
if (i > 0) group_clause += ", ";
// Re-emit la expression para GROUP BY (no alias).
group_clause += emit_breakout_expr(stg.breakouts[i]);
}
// ORDER BY
std::string order_clause;
for (size_t i = 0; i < stg.sorts.size(); ++i) {
if (i > 0) order_clause += ", ";
order_clause += sql_ident(stg.sorts[i].col) + (stg.sorts[i].desc ? " DESC" : " ASC");
}
std::string cte = ",\n" + cur + " AS (\n SELECT " + select_list
+ "\n FROM " + prev;
if (!group_clause.empty()) cte += "\n GROUP BY " + group_clause;
if (!order_clause.empty()) cte += "\n ORDER BY " + order_clause;
cte += "\n)";
e.sql += cte;
return true;
}
} // anon
SqlEmit emit_sql(const State& state,
const std::vector<TableInput>& tables,
int up_to_stage) {
SqlEmit out;
if (state.stages.empty()) {
out.error = "state has no stages";
return out;
}
if (tables.empty()) {
out.error = "no input tables provided";
return out;
}
int target = (up_to_stage < 0) ? state.active_stage : up_to_stage;
if (target < 0) target = 0;
if (target >= (int)state.stages.size()) target = (int)state.stages.size() - 1;
// Resolve main idx via state.main_source (o tables[0] default).
int main_idx = resolve_main_idx(tables, state.main_source);
if (main_idx < 0) main_idx = 0;
if (!emit_stage0(state, tables, main_idx, out)) return out;
for (int si = 1; si <= target; ++si) {
if (!emit_stage_n(state.stages[(size_t)si], si, out)) return out;
}
out.sql += "\nSELECT * FROM t" + std::to_string(target) + ";\n";
return out;
}
} // namespace tql_to_sql
@@ -1,41 +0,0 @@
// tql_to_sql: emite SQL DuckDB equivalente a una pipeline TQL State.
// Pure. Sin DuckDB linkado. Solo string emit + validacion.
// Ver issue 0080 + docs/TQL.md (seccion "SQL transpile subset").
#pragma once
#include "data_table_logic.h"
#include <string>
#include <vector>
namespace tql_to_sql {
struct SqlEmit {
std::string sql; // SELECT/CTE chain DuckDB
std::vector<std::string> params; // bound values posicionales (?)
std::vector<std::string> warnings; // soft issues (col not found, etc.)
std::string error; // si non-empty, emit fallo
};
// Pure: emite SQL DuckDB equivalente a stages 0..active del state.
// `tables` provee schema (headers/types/name) de cada TableInput. El caller
// es responsable de hidratar las tablas en DuckDB con esos nombres.
// `up_to_stage = -1` => state.active_stage.
SqlEmit emit_sql(const data_table::State& state,
const std::vector<data_table::TableInput>& tables,
int up_to_stage = -1);
// Pure: valida que `formula` (cuerpo Lua de un derived col) este dentro del
// subset SQL-transpilable. Si valido, retorna true. Si no, false + razon
// concreta en `error_out` (categoria + token problematico).
// Ver docs/TQL.md#sql-transpile-subset.
bool is_transpilable(const std::string& formula, std::string& error_out);
// Pure: transpila formula Lua subset -> SQL expression. Si fuera de subset,
// retorna "" y rellena `error_out`. Asume is_transpilable retornaria true.
// `in_headers` necesario para resolver `[col]` refs y emitir identifier
// SQL apropiado (quoted si tiene char especial).
std::string transpile_expr(const std::string& formula,
const std::vector<std::string>& in_headers,
std::string& error_out);
} // namespace tql_to_sql
@@ -1,870 +0,0 @@
#include "viz.h"
#include "implot.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <unordered_map>
#include <vector>
namespace viz {
using data_table::StageOutput;
using data_table::ColumnType;
using data_table::ViewMode;
using data_table::ViewConfig;
using data_table::parse_number;
using data_table::nearest_index_2d;
using data_table::pie_angle;
using data_table::pie_slice_at_angle;
using data_table::heatmap_cell_at;
static int find_header(const StageOutput& out, const std::string& name) {
if (name.empty()) return -1;
for (size_t c = 0; c < out.headers.size(); ++c)
if (out.headers[c] == name) return (int)c;
return -1;
}
static int resolve_x(const StageOutput& out, const ViewConfig& cfg, int fallback) {
int c = find_header(out, cfg.x_col);
return (c >= 0) ? c : fallback;
}
static int resolve_cat(const StageOutput& out, const ViewConfig& cfg, int fallback) {
int c = find_header(out, cfg.cat_col);
return (c >= 0) ? c : fallback;
}
static int resolve_size(const StageOutput& out, const ViewConfig& cfg, int fallback) {
int c = find_header(out, cfg.size_col);
return (c >= 0) ? c : fallback;
}
int first_numeric_col(const StageOutput& out) {
for (size_t c = 0; c < out.types.size(); ++c) {
if (out.types[c] == ColumnType::Int || out.types[c] == ColumnType::Float) return (int)c;
}
return -1;
}
int first_category_col(const StageOutput& out) {
for (size_t c = 0; c < out.types.size(); ++c) {
ColumnType t = out.types[c];
if (t == ColumnType::String || t == ColumnType::Date || t == ColumnType::Bool ||
t == ColumnType::Json) return (int)c;
}
return -1;
}
std::vector<double> extract_numeric(const StageOutput& out, int col) {
std::vector<double> v;
if (col < 0 || col >= out.cols) return v;
v.reserve(out.rows);
for (int r = 0; r < out.rows; ++r) {
const char* s = out.cells[(size_t)r * out.cols + col];
double d = 0;
if (s && *s && parse_number(s, d)) v.push_back(d);
else v.push_back(std::nan(""));
}
return v;
}
std::vector<std::string> extract_category(const StageOutput& out, int col) {
std::vector<std::string> v;
if (col < 0 || col >= out.cols) return v;
v.reserve(out.rows);
for (int r = 0; r < out.rows; ++r) {
const char* s = out.cells[(size_t)r * out.cols + col];
v.emplace_back(s ? s : "");
}
return v;
}
namespace {
struct NumCol { int idx; std::string name; std::vector<double> vals; };
std::vector<NumCol> collect_numeric(const StageOutput& out, int max_n = 16) {
std::vector<NumCol> r;
for (size_t c = 0; c < out.types.size() && (int)r.size() < max_n; ++c) {
if (out.types[c] == ColumnType::Int || out.types[c] == ColumnType::Float) {
NumCol nc;
nc.idx = (int)c;
nc.name = out.headers[c];
nc.vals = extract_numeric(out, (int)c);
r.push_back(std::move(nc));
}
}
return r;
}
std::vector<NumCol> collect_numeric_filtered(const StageOutput& out,
const ViewConfig& cfg,
int max_n = 16) {
if (cfg.y_cols.empty()) return collect_numeric(out, max_n);
std::vector<NumCol> r;
for (const auto& name : cfg.y_cols) {
if ((int)r.size() >= max_n) break;
int c = find_header(out, name);
if (c < 0) continue;
if (out.types[c] != ColumnType::Int && out.types[c] != ColumnType::Float) continue;
NumCol nc;
nc.idx = c;
nc.name = out.headers[c];
nc.vals = extract_numeric(out, c);
r.push_back(std::move(nc));
}
if (r.empty()) return collect_numeric(out, max_n);
return r;
}
ImPlotSpec spec_with_color(unsigned int rgba_color) {
if (rgba_color == 0) return ImPlotSpec();
ImU32 c = (ImU32)rgba_color;
return ImPlotSpec(ImPlotProp_LineColor, c, ImPlotProp_FillColor, c);
}
// Axis flags: locked = no pan/zoom; unlocked = 0 (sin AutoFit, para preservar
// pan/zoom del user). Re-fit explicito via SetNextAxesToFit cuando fit_request.
ImPlotAxisFlags axflag(const ViewConfig& cfg, ImPlotAxisFlags base = 0) {
if (cfg.locked) return base | ImPlotAxisFlags_Lock;
return base;
}
// Llamar antes de BeginPlot. Si cfg.fit_request -> fuerza re-fit y limpia el flag.
void maybe_fit(const ViewConfig& cfg) {
if (cfg.fit_request) {
ImPlot::SetNextAxesToFit();
cfg.fit_request = false;
}
}
void info_text(const char* msg) {
ImVec2 avail = ImGui::GetContentRegionAvail();
ImVec2 sz = ImGui::CalcTextSize(msg);
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + (avail.x - sz.x) * 0.5f,
ImGui::GetCursorPosY() + (avail.y - sz.y) * 0.5f));
ImGui::TextDisabled("%s", msg);
}
// Drop NaN and pair with optional labels.
std::vector<double> finite(const std::vector<double>& v) {
std::vector<double> r; r.reserve(v.size());
for (double d : v) if (!std::isnan(d)) r.push_back(d);
return r;
}
bool render_bar_like(const StageOutput& out, ViewMode mode,
const ViewConfig& cfg, ImVec2 size,
int* clicked_row_out = nullptr) {
int cat_col = resolve_cat(out, cfg, first_category_col(out));
auto nums = collect_numeric_filtered(out, cfg, 8);
if (cat_col < 0 || nums.empty()) {
info_text("Need 1 category + 1+ numeric columns");
return false;
}
auto cats = extract_category(out, cat_col);
int n = (int)cats.size();
if (n == 0) { info_text("Empty data"); return false; }
// Ticks
std::vector<double> ticks(n);
std::vector<const char*> labels(n);
for (int i = 0; i < n; ++i) { ticks[i] = i; labels[i] = cats[i].c_str(); }
bool horiz = (mode == ViewMode::Bar);
ImPlotFlags pflags = cfg.show_legend ? 0 : ImPlotFlags_NoLegend;
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##bar", size, pflags)) return false;
ImPlotAxisFlags ax_cat = axflag(cfg);
ImPlotAxisFlags ax_num = axflag(cfg);
if (horiz) {
ImPlot::SetupAxes(out.headers[nums[0].idx].c_str(), out.headers[cat_col].c_str(),
ax_num, ax_cat);
ImPlot::SetupAxisTicks(ImAxis_Y1, ticks.data(), n, labels.data(), false);
} else {
ImPlot::SetupAxes(out.headers[cat_col].c_str(), out.headers[nums[0].idx].c_str(),
ax_cat, ax_num);
ImPlot::SetupAxisTicks(ImAxis_X1, ticks.data(), n, labels.data(), false);
}
if (mode == ViewMode::StackedBar || mode == ViewMode::GroupedBar) {
// Build flat matrix items x groups
int items = (int)nums.size();
std::vector<double> mat((size_t)items * n, 0.0);
std::vector<const char*> series_labels(items);
for (int it = 0; it < items; ++it) {
series_labels[it] = nums[it].name.c_str();
for (int g = 0; g < n; ++g) {
double d = nums[it].vals[g];
mat[(size_t)it * n + g] = std::isnan(d) ? 0.0 : d;
}
}
int flags = (mode == ViewMode::StackedBar) ? ImPlotBarGroupsFlags_Stacked : 0;
if (horiz) flags |= ImPlotBarGroupsFlags_Horizontal;
ImPlot::PlotBarGroups(series_labels.data(), mat.data(), items, n, 0.67, 0,
ImPlotSpec(ImPlotProp_Flags, flags));
} else {
// Single series (first numeric col).
std::vector<double> ys(n);
for (int i = 0; i < n; ++i) {
double d = nums[0].vals[i];
ys[i] = std::isnan(d) ? 0.0 : d;
}
ImPlotSpec spc = spec_with_color(cfg.primary_color);
if (horiz) {
if (cfg.primary_color != 0) {
ImU32 col = (ImU32)cfg.primary_color;
ImPlot::PlotBars(nums[0].name.c_str(), ys.data(), ticks.data(), n, 0.67,
ImPlotSpec(ImPlotProp_Flags, ImPlotBarsFlags_Horizontal,
ImPlotProp_FillColor, col,
ImPlotProp_LineColor, col));
} else {
ImPlot::PlotBars(nums[0].name.c_str(), ys.data(), ticks.data(), n, 0.67,
ImPlotSpec(ImPlotProp_Flags, ImPlotBarsFlags_Horizontal));
}
} else {
ImPlot::PlotBars(nums[0].name.c_str(), ticks.data(), ys.data(), n, 0.67, spc);
}
}
// Hit-test fase 10: idx = round(plot.{x|y}) en single-series mode.
if (clicked_row_out &&
mode != ViewMode::GroupedBar && mode != ViewMode::StackedBar &&
ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImPlotPoint p = ImPlot::GetPlotMousePos();
double target = horiz ? p.y : p.x;
int idx = (int)(target + 0.5);
if (idx >= 0 && idx < n) *clicked_row_out = idx;
}
ImPlot::EndPlot();
return true;
}
bool render_line_like(const StageOutput& out, ViewMode mode,
const ViewConfig& cfg, ImVec2 size) {
auto nums = collect_numeric_filtered(out, cfg, 8);
if (nums.empty()) { info_text("Need at least 1 numeric column"); return false; }
ImPlotFlags pflags = cfg.show_legend ? 0 : ImPlotFlags_NoLegend;
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##line", size, pflags)) return false;
ImPlot::SetupAxes(nullptr, nullptr, axflag(cfg), axflag(cfg));
int n = nums.empty() ? 0 : (int)nums[0].vals.size();
if (n == 0) { ImPlot::EndPlot(); return false; }
// X column: cfg.x_col override; sino primer numeric si hay >=2; sino indices.
int x_idx = -1;
if (!cfg.x_col.empty()) {
int xc = find_header(out, cfg.x_col);
if (xc >= 0 && (out.types[xc] == ColumnType::Int || out.types[xc] == ColumnType::Float)) {
x_idx = xc;
}
}
std::vector<double> idx_xs;
const double* xs = nullptr;
int start_y = 0;
std::vector<double> x_data_external;
if (x_idx >= 0) {
x_data_external = extract_numeric(out, x_idx);
xs = x_data_external.data();
} else if (nums.size() >= 2 && cfg.y_cols.empty()) {
xs = nums[0].vals.data();
start_y = 1;
} else {
idx_xs.resize(n);
for (int i = 0; i < n; ++i) idx_xs[i] = i;
xs = idx_xs.data();
}
bool only_one = (cfg.primary_color != 0) && (nums.size() - start_y == 1);
for (size_t i = (size_t)start_y; i < nums.size(); ++i) {
const auto& nc = nums[i];
ImU32 col = only_one ? (ImU32)cfg.primary_color : 0;
int marker = cfg.show_markers ? ImPlotMarker_Circle : ImPlotMarker_None;
if (mode == ViewMode::Area) {
if (col) {
ImPlot::PlotShaded(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(), 0.0,
ImPlotSpec(ImPlotProp_FillColor, col, ImPlotProp_LineColor, col));
} else {
ImPlot::PlotShaded(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(), 0.0);
}
} else if (mode == ViewMode::Stairs) {
if (col) {
ImPlot::PlotStairs(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(),
ImPlotSpec(ImPlotProp_LineColor, col));
} else {
ImPlot::PlotStairs(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size());
}
} else {
if (col) {
ImPlot::PlotLine(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(),
ImPlotSpec(ImPlotProp_Flags, ImPlotLineFlags_SkipNaN,
ImPlotProp_LineColor, col,
ImPlotProp_Marker, marker));
} else {
ImPlot::PlotLine(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(),
ImPlotSpec(ImPlotProp_Flags, ImPlotLineFlags_SkipNaN,
ImPlotProp_Marker, marker));
}
}
}
ImPlot::EndPlot();
return true;
}
bool render_scatter(const StageOutput& out, const ViewConfig& cfg, ImVec2 size,
int* clicked_row_out = nullptr) {
// Soporte cfg.x_col + cfg.y_cols[0]
int xc = find_header(out, cfg.x_col);
int yc = !cfg.y_cols.empty() ? find_header(out, cfg.y_cols[0]) : -1;
std::vector<NumCol> nums;
if (xc >= 0 && yc >= 0) {
NumCol a{xc, out.headers[xc], extract_numeric(out, xc)};
NumCol b{yc, out.headers[yc], extract_numeric(out, yc)};
nums = {a, b};
} else {
nums = collect_numeric(out, 4);
}
if (nums.size() < 2) { info_text("Need 2 numeric columns"); return false; }
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##scatter", size, cfg.show_legend ? 0 : ImPlotFlags_NoLegend)) return false;
ImPlot::SetupAxes(nums[0].name.c_str(), nums[1].name.c_str(),
axflag(cfg), axflag(cfg));
if (cfg.primary_color) {
ImU32 col = (ImU32)cfg.primary_color;
ImPlot::PlotScatter("##s", nums[0].vals.data(), nums[1].vals.data(),
(int)nums[0].vals.size(),
ImPlotSpec(ImPlotProp_MarkerFillColor, col,
ImPlotProp_MarkerLineColor, col));
} else {
ImPlot::PlotScatter("##s", nums[0].vals.data(), nums[1].vals.data(),
(int)nums[0].vals.size());
}
if (clicked_row_out &&
ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImPlotPoint p = ImPlot::GetPlotMousePos();
int idx = nearest_index_2d(p.x, p.y,
nums[0].vals.data(), nums[1].vals.data(),
(int)nums[0].vals.size());
if (idx >= 0) *clicked_row_out = idx;
}
ImPlot::EndPlot();
return true;
}
bool render_bubble(const StageOutput& out, const ViewConfig& cfg, ImVec2 size,
int* clicked_row_out = nullptr) {
int xc = find_header(out, cfg.x_col);
int yc = !cfg.y_cols.empty() ? find_header(out, cfg.y_cols[0]) : -1;
int sc = resolve_size(out, cfg, -1);
std::vector<NumCol> nums;
if (xc >= 0 && yc >= 0 && sc >= 0) {
nums = {
{xc, out.headers[xc], extract_numeric(out, xc)},
{yc, out.headers[yc], extract_numeric(out, yc)},
{sc, out.headers[sc], extract_numeric(out, sc)},
};
} else {
nums = collect_numeric(out, 4);
}
if (nums.size() < 3) { info_text("Need 3 numeric columns (x, y, size)"); return false; }
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##bubble", size, cfg.show_legend ? 0 : ImPlotFlags_NoLegend)) return false;
ImPlot::SetupAxes(nums[0].name.c_str(), nums[1].name.c_str(),
axflag(cfg), axflag(cfg));
ImPlot::PlotBubbles("##b", nums[0].vals.data(), nums[1].vals.data(),
nums[2].vals.data(), (int)nums[0].vals.size());
if (clicked_row_out &&
ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImPlotPoint p = ImPlot::GetPlotMousePos();
int idx = nearest_index_2d(p.x, p.y,
nums[0].vals.data(), nums[1].vals.data(),
(int)nums[0].vals.size());
if (idx >= 0) *clicked_row_out = idx;
}
ImPlot::EndPlot();
return true;
}
bool render_histogram(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
auto nums = collect_numeric_filtered(out, cfg, 4);
if (nums.empty()) { info_text("Need 1 numeric column"); return false; }
auto vals = finite(nums[0].vals);
if (vals.empty()) { info_text("No finite values"); return false; }
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##hist", size, cfg.show_legend ? 0 : ImPlotFlags_NoLegend)) return false;
ImPlot::SetupAxes(nums[0].name.c_str(), "count",
axflag(cfg), axflag(cfg));
int bins = (cfg.hist_bins > 0) ? cfg.hist_bins : ImPlotBin_Sturges;
if (cfg.primary_color) {
ImU32 col = (ImU32)cfg.primary_color;
ImPlot::PlotHistogram("##h", vals.data(), (int)vals.size(), bins, 1.0,
ImPlotRange(),
ImPlotSpec(ImPlotProp_FillColor, col,
ImPlotProp_LineColor, col));
} else {
ImPlot::PlotHistogram("##h", vals.data(), (int)vals.size(), bins);
}
ImPlot::EndPlot();
return true;
}
bool render_hist2d(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
int xc = find_header(out, cfg.x_col);
int yc = !cfg.y_cols.empty() ? find_header(out, cfg.y_cols[0]) : -1;
std::vector<NumCol> nums;
if (xc >= 0 && yc >= 0) {
nums = {
{xc, out.headers[xc], extract_numeric(out, xc)},
{yc, out.headers[yc], extract_numeric(out, yc)},
};
} else {
nums = collect_numeric(out, 2);
}
if (nums.size() < 2) { info_text("Need 2 numeric columns"); return false; }
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##hist2d", size, cfg.show_legend ? 0 : ImPlotFlags_NoLegend)) return false;
ImPlot::SetupAxes(nums[0].name.c_str(), nums[1].name.c_str());
int bins = (cfg.hist_bins > 0) ? cfg.hist_bins : ImPlotBin_Sturges;
ImPlot::PlotHistogram2D("##h2", nums[0].vals.data(), nums[1].vals.data(),
(int)nums[0].vals.size(), bins, bins);
ImPlot::EndPlot();
return true;
}
bool render_heatmap(const StageOutput& out, const ViewConfig& cfg, ImVec2 size,
int* clicked_row_out = nullptr) {
auto nums = collect_numeric_filtered(out, cfg, 64);
if (nums.empty()) { info_text("Need numeric columns"); return false; }
int cols = (int)nums.size();
int rows = (int)nums[0].vals.size();
if (rows == 0) { info_text("No rows"); return false; }
std::vector<double> mat((size_t)rows * cols, 0.0);
double mn = +1e300, mx = -1e300;
for (int c = 0; c < cols; ++c) {
for (int r = 0; r < rows; ++r) {
double d = nums[c].vals[r];
if (std::isnan(d)) d = 0;
mat[(size_t)r * cols + c] = d;
if (d < mn) mn = d; if (d > mx) mx = d;
}
}
if (mn == mx) { mx = mn + 1; }
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##heatmap", size, 0)) return false;
ImPlot::PlotHeatmap("##hm", mat.data(), rows, cols, mn, mx, nullptr);
if (clicked_row_out &&
ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImPlotPoint p = ImPlot::GetPlotMousePos();
// ImPlot heatmap Y se pinta de top a bottom; plot mouse_y va igual
// (default scale 0..rows). Mapeo directo.
int rr, cc;
heatmap_cell_at(p.x, p.y, rows, cols, rr, cc);
if (rr >= 0) *clicked_row_out = rr;
(void)cc;
}
ImPlot::EndPlot();
return true;
}
bool render_pie(const StageOutput& out, const ViewConfig& cfg, bool donut, ImVec2 size,
int* clicked_row_out = nullptr) {
int cat = resolve_cat(out, cfg, first_category_col(out));
auto nums = collect_numeric_filtered(out, cfg, 1);
if (cat < 0 || nums.empty()) { info_text("Need 1 category + 1 numeric"); return false; }
auto cats = extract_category(out, cat);
int n = std::min((int)cats.size(), (int)nums[0].vals.size());
if (n == 0) return false;
std::vector<double> values(n);
std::vector<const char*> labels(n);
for (int i = 0; i < n; ++i) {
double d = nums[0].vals[i];
values[i] = std::isnan(d) ? 0.0 : std::abs(d);
labels[i] = cats[i].c_str();
}
ImPlotFlags pf = ImPlotFlags_Equal;
if (!cfg.show_legend) pf |= ImPlotFlags_NoLegend;
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##pie", size, pf)) return false;
ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations,
ImPlotAxisFlags_NoDecorations);
ImPlot::SetupAxesLimits(0, 1, 0, 1, ImPlotCond_Always);
double radius = (cfg.pie_radius > 0) ? cfg.pie_radius : (donut ? 0.4 : 0.45);
ImPlot::PlotPieChart(labels.data(), values.data(), n, 0.5, 0.5, radius, "%.1f");
if (donut) {
// Draw inner hole as solid circle by overlaying a smaller pie of one slice transparent.
// Simpler: just visually it's a circle with text. Use no extra primitive for now.
}
if (clicked_row_out &&
ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImPlotPoint p = ImPlot::GetPlotMousePos();
double dx = p.x - 0.5, dy = p.y - 0.5;
double dist2 = dx*dx + dy*dy;
double inner = donut ? (radius * 0.5) : 0.0;
if (dist2 <= radius * radius && dist2 >= inner * inner) {
double ang = pie_angle(0.5, 0.5, p.x, p.y);
int idx = pie_slice_at_angle(ang, values.data(), n);
if (idx >= 0) *clicked_row_out = idx;
}
}
ImPlot::EndPlot();
return true;
}
bool render_funnel(const StageOutput& out, const ViewConfig& cfg, ImVec2 size,
int* clicked_row_out = nullptr) {
int cat = resolve_cat(out, cfg, first_category_col(out));
auto nums = collect_numeric_filtered(out, cfg, 1);
if (cat < 0 || nums.empty()) { info_text("Need 1 category + 1 numeric"); return false; }
auto cats = extract_category(out, cat);
int n = std::min((int)cats.size(), (int)nums[0].vals.size());
if (n == 0) return false;
// Sort desc by value
std::vector<int> idx(n);
for (int i = 0; i < n; ++i) idx[i] = i;
std::sort(idx.begin(), idx.end(), [&](int a, int b) {
double da = std::isnan(nums[0].vals[a]) ? -1e300 : nums[0].vals[a];
double db = std::isnan(nums[0].vals[b]) ? -1e300 : nums[0].vals[b];
return da > db;
});
std::vector<double> ys(n);
std::vector<double> ticks(n);
std::vector<const char*> labels(n);
std::vector<std::string> labels_store(n);
for (int i = 0; i < n; ++i) {
double d = nums[0].vals[idx[i]];
ys[i] = std::isnan(d) ? 0 : d;
ticks[i] = n - 1 - i; // descending order
labels_store[i] = cats[idx[i]];
labels[i] = labels_store[i].c_str();
}
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##funnel", size, 0)) return false;
ImPlot::SetupAxes(nums[0].name.c_str(), out.headers[cat].c_str(),
axflag(cfg), axflag(cfg));
ImPlot::SetupAxisTicks(ImAxis_Y1, ticks.data(), n, labels.data(), false);
ImPlot::PlotBars(nums[0].name.c_str(), ys.data(), ticks.data(), n, 0.85,
ImPlotSpec(ImPlotProp_Flags, ImPlotBarsFlags_Horizontal));
if (clicked_row_out &&
ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImPlotPoint p = ImPlot::GetPlotMousePos();
int tick_idx = (int)(p.y + 0.5);
// ticks[i] = n-1-i. Invertir para idx en orden sorted descendiente.
int sorted_pos = (n - 1) - tick_idx;
if (sorted_pos >= 0 && sorted_pos < n) {
// idx[sorted_pos] da indice de row original en out.
*clicked_row_out = idx[sorted_pos];
}
}
ImPlot::EndPlot();
return true;
}
bool render_waterfall(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
auto nums = collect_numeric_filtered(out, cfg, 1);
if (nums.empty()) { info_text("Need 1 numeric column"); return false; }
int n = (int)nums[0].vals.size();
if (n == 0) return false;
int cat = resolve_cat(out, cfg, first_category_col(out));
auto cats = (cat >= 0) ? extract_category(out, cat) : std::vector<std::string>();
std::vector<double> running(n + 1, 0);
for (int i = 0; i < n; ++i) {
double d = std::isnan(nums[0].vals[i]) ? 0 : nums[0].vals[i];
running[i + 1] = running[i] + d;
}
std::vector<double> ticks(n);
for (int i = 0; i < n; ++i) ticks[i] = i;
std::vector<const char*> labels(n);
for (int i = 0; i < n; ++i) labels[i] = (i < (int)cats.size()) ? cats[i].c_str() : "";
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##waterfall", size, 0)) return false;
ImPlot::SetupAxes(nullptr, nums[0].name.c_str(),
axflag(cfg), axflag(cfg));
if (cat >= 0) ImPlot::SetupAxisTicks(ImAxis_X1, ticks.data(), n, labels.data(), false);
// Draw stems with rectangles via error-bars trick: low=cum_prev, high=cum_curr.
std::vector<double> mid(n), err(n);
for (int i = 0; i < n; ++i) {
mid[i] = (running[i] + running[i + 1]) * 0.5;
err[i] = std::abs((running[i + 1] - running[i]) * 0.5);
}
ImPlot::PlotErrorBars("##wf", ticks.data(), mid.data(), err.data(), n);
ImPlot::PlotLine("cum", running.data() + 1, n);
ImPlot::EndPlot();
return true;
}
bool render_kpi_single(const StageOutput& out, const ViewConfig& cfg) {
int nc = !cfg.y_cols.empty() ? find_header(out, cfg.y_cols[0]) : -1;
if (nc < 0) nc = first_numeric_col(out);
if (nc < 0) { info_text("Need 1 numeric column"); return false; }
auto vals = extract_numeric(out, nc);
if (vals.empty()) { info_text("Empty"); return false; }
double last = std::nan("");
for (auto v : vals) if (!std::isnan(v)) last = v;
if (std::isnan(last)) { info_text("No finite values"); return false; }
char buf[64];
if (std::abs(last) >= 1e6) std::snprintf(buf, sizeof(buf), "%.2fM", last / 1e6);
else if (std::abs(last) >= 1e3) std::snprintf(buf, sizeof(buf), "%.2fK", last / 1e3);
else std::snprintf(buf, sizeof(buf), "%.3g", last);
ImVec2 avail = ImGui::GetContentRegionAvail();
ImGui::SetWindowFontScale(4.0f);
ImVec2 sz = ImGui::CalcTextSize(buf);
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + (avail.x - sz.x) * 0.5f,
ImGui::GetCursorPosY() + (avail.y - sz.y) * 0.5f - 20));
ImGui::TextUnformatted(buf);
ImGui::SetWindowFontScale(1.0f);
sz = ImGui::CalcTextSize(out.headers[nc].c_str());
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + (avail.x - sz.x) * 0.5f,
ImGui::GetCursorPosY() + (avail.y - sz.y) * 0.5f - 10));
ImGui::TextDisabled("%s", out.headers[nc].c_str());
return true;
}
bool render_kpi_grid(const StageOutput& out, const ViewConfig& cfg) {
auto nums = collect_numeric_filtered(out, cfg, 12);
if (nums.empty()) { info_text("Need numeric columns"); return false; }
ImVec2 avail = ImGui::GetContentRegionAvail();
int per_row = std::max(1, (int)(avail.x / 220));
int idx = 0;
for (auto& nc : nums) {
double last = std::nan("");
for (auto v : nc.vals) if (!std::isnan(v)) last = v;
if (std::isnan(last)) last = 0;
char buf[64];
if (std::abs(last) >= 1e6) std::snprintf(buf, sizeof(buf), "%.2fM", last / 1e6);
else if (std::abs(last) >= 1e3) std::snprintf(buf, sizeof(buf), "%.2fK", last / 1e3);
else std::snprintf(buf, sizeof(buf), "%.4g", last);
ImGui::BeginChild((ImGuiID)(0x1000 + idx), ImVec2(210, 100), true);
ImGui::TextDisabled("%s", nc.name.c_str());
ImGui::SetWindowFontScale(2.4f);
ImGui::TextUnformatted(buf);
ImGui::SetWindowFontScale(1.0f);
ImGui::EndChild();
if ((idx % per_row) != (per_row - 1)) ImGui::SameLine();
idx++;
}
return true;
}
bool render_stem(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
auto nums = collect_numeric_filtered(out, cfg, 1);
if (nums.empty()) { info_text("Need 1 numeric column"); return false; }
int n = (int)nums[0].vals.size();
std::vector<double> xs(n); for (int i = 0; i < n; ++i) xs[i] = i;
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##stem", size, 0)) return false;
ImPlot::SetupAxes(nullptr, nums[0].name.c_str(),
axflag(cfg), axflag(cfg));
ImPlot::PlotStems(nums[0].name.c_str(), xs.data(), nums[0].vals.data(), n);
ImPlot::EndPlot();
return true;
}
bool render_errorbars(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
auto nums = collect_numeric_filtered(out, cfg, 4);
if (nums.size() < 2) { info_text("Need 2 numeric columns (value, err)"); return false; }
int n = (int)nums[0].vals.size();
std::vector<double> xs(n); for (int i = 0; i < n; ++i) xs[i] = i;
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##eb", size, 0)) return false;
ImPlot::SetupAxes(nullptr, nums[0].name.c_str(),
axflag(cfg), axflag(cfg));
ImPlot::PlotErrorBars(nums[0].name.c_str(), xs.data(),
nums[0].vals.data(), nums[1].vals.data(), n);
ImPlot::PlotScatter("##s", xs.data(), nums[0].vals.data(), n);
ImPlot::EndPlot();
return true;
}
// BoxPlot: agrupar por categoria, calcular min/p25/p50/p75/max y dibujar
// rectangulos manuales via PlotShaded + lineas.
bool render_boxplot(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
int cat = resolve_cat(out, cfg, first_category_col(out));
auto nums = collect_numeric_filtered(out, cfg, 1);
if (cat < 0 || nums.empty()) { info_text("Need 1 category + 1 numeric"); return false; }
auto cats = extract_category(out, cat);
int n = std::min((int)cats.size(), (int)nums[0].vals.size());
if (n == 0) return false;
// Group values by category
std::unordered_map<std::string, std::vector<double>> groups;
std::vector<std::string> order;
for (int i = 0; i < n; ++i) {
if (groups.find(cats[i]) == groups.end()) order.push_back(cats[i]);
double d = nums[0].vals[i];
if (!std::isnan(d)) groups[cats[i]].push_back(d);
}
int G = (int)order.size();
if (G == 0) return false;
std::vector<double> mn(G), p25(G), p50(G), p75(G), mx(G), xs(G);
std::vector<const char*> labels(G);
for (int g = 0; g < G; ++g) {
auto& v = groups[order[g]];
std::sort(v.begin(), v.end());
int N = (int)v.size();
xs[g] = g;
labels[g]= order[g].c_str();
if (N == 0) { mn[g]=p25[g]=p50[g]=p75[g]=mx[g]=0; continue; }
mn[g] = v.front();
mx[g] = v.back();
p25[g] = v[std::min(N - 1, (int)(N * 0.25))];
p50[g] = v[std::min(N - 1, (int)(N * 0.50))];
p75[g] = v[std::min(N - 1, (int)(N * 0.75))];
}
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##box", size, 0)) return false;
ImPlot::SetupAxes(out.headers[cat].c_str(), nums[0].name.c_str(),
axflag(cfg), axflag(cfg));
ImPlot::SetupAxisTicks(ImAxis_X1, xs.data(), G, labels.data(), false);
// Whiskers: stems from min to max
for (int g = 0; g < G; ++g) {
double lo[2] = { mn[g], mx[g] };
double xx[2] = { xs[g], xs[g] };
ImPlot::PlotLine("##wh", xx, lo, 2);
}
// Box: p25..p75 as bars centered on p50
std::vector<double> mid(G), half(G);
for (int g = 0; g < G; ++g) {
mid[g] = (p25[g] + p75[g]) * 0.5;
half[g] = (p75[g] - p25[g]) * 0.5;
}
ImPlot::PlotErrorBars("box", xs.data(), mid.data(), half.data(), G);
ImPlot::PlotScatter("median", xs.data(), p50.data(), G);
ImPlot::EndPlot();
return true;
}
// Candlestick: tiempo + O/H/L/C. Asume 4 primeras cols numericas en ese orden.
bool render_candlestick(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
auto nums = collect_numeric_filtered(out, cfg, 8);
if (nums.size() < 4) { info_text("Need 4 numeric columns: O/H/L/C"); return false; }
int n = (int)nums[0].vals.size();
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##candle", size, 0)) return false;
ImPlot::SetupAxes("t", "price", axflag(cfg), axflag(cfg));
std::vector<double> xs(n); for (int i = 0; i < n; ++i) xs[i] = i;
const auto& O = nums[0].vals;
const auto& H = nums[1].vals;
const auto& L = nums[2].vals;
const auto& C = nums[3].vals;
// Wicks
for (int i = 0; i < n; ++i) {
double xx[2] = { xs[i], xs[i] };
double yy[2] = { L[i], H[i] };
ImPlot::PlotLine("##wick", xx, yy, 2);
}
// Body via PlotBars(mid, |C-O|)? Simpler: separate lines.
std::vector<double> body_low(n), body_high(n), body_mid(n), body_err(n);
for (int i = 0; i < n; ++i) {
body_low[i] = std::min(O[i], C[i]);
body_high[i] = std::max(O[i], C[i]);
body_mid[i] = (body_low[i] + body_high[i]) * 0.5;
body_err[i] = (body_high[i] - body_low[i]) * 0.5;
}
ImPlot::PlotErrorBars("OHLC", xs.data(), body_mid.data(), body_err.data(), n);
ImPlot::EndPlot();
return true;
}
bool render_radar(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
auto nums = collect_numeric_filtered(out, cfg, 12);
if (nums.size() < 3) { info_text("Need 3+ numeric columns"); return false; }
int K = (int)nums.size();
int n = (int)nums[0].vals.size();
if (n == 0) return false;
// Take first row as the polygon.
std::vector<double> xs(K + 1), ys(K + 1);
double radius_norm = 0;
for (int k = 0; k < K; ++k) {
double d = nums[k].vals[0];
if (std::isnan(d)) d = 0;
radius_norm = std::max(radius_norm, std::abs(d));
}
if (radius_norm == 0) radius_norm = 1;
for (int k = 0; k < K; ++k) {
double v = nums[k].vals[0]; if (std::isnan(v)) v = 0;
double angle = 2 * 3.14159265358979 * k / K - 3.14159265358979 / 2;
double r = v / radius_norm;
xs[k] = std::cos(angle) * r;
ys[k] = std::sin(angle) * r;
}
xs[K] = xs[0]; ys[K] = ys[0];
maybe_fit(cfg);
if (!ImPlot::BeginPlot("##radar", size,
ImPlotFlags_Equal | ImPlotFlags_NoLegend)) return false;
ImPlot::SetupAxesLimits(-1.2, 1.2, -1.2, 1.2, ImPlotCond_Always);
ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations,
ImPlotAxisFlags_NoDecorations);
// Grid rings
for (double rr : {0.25, 0.5, 0.75, 1.0}) {
double gx[64], gy[64];
for (int i = 0; i < 64; ++i) {
double a = 2 * 3.14159265358979 * i / 63;
gx[i] = std::cos(a) * rr; gy[i] = std::sin(a) * rr;
}
ImPlot::PlotLine("##grid", gx, gy, 64);
}
ImPlot::PlotLine("radar", xs.data(), ys.data(), K + 1);
// Axis labels
for (int k = 0; k < K; ++k) {
double a = 2 * 3.14159265358979 * k / K - 3.14159265358979 / 2;
ImPlot::PlotText(nums[k].name.c_str(), std::cos(a) * 1.1, std::sin(a) * 1.1);
}
ImPlot::EndPlot();
return true;
}
} // anon
bool render(const StageOutput& out, ViewMode mode,
const ViewConfig& cfg, ImVec2 size,
int* clicked_row_out) {
if (clicked_row_out) *clicked_row_out = -1;
if (out.rows == 0 || out.cols == 0) {
info_text("No data");
return false;
}
switch (mode) {
case ViewMode::Table: return false;
case ViewMode::Bar:
case ViewMode::Column:
case ViewMode::GroupedBar:
case ViewMode::StackedBar: return render_bar_like(out, mode, cfg, size, clicked_row_out);
case ViewMode::Line:
case ViewMode::Area:
case ViewMode::Stairs: return render_line_like(out, mode, cfg, size);
case ViewMode::Scatter: return render_scatter(out, cfg, size, clicked_row_out);
case ViewMode::Bubble: return render_bubble(out, cfg, size, clicked_row_out);
case ViewMode::Histogram: return render_histogram(out, cfg, size);
case ViewMode::Histogram2D: return render_hist2d(out, cfg, size);
case ViewMode::Heatmap: return render_heatmap(out, cfg, size, clicked_row_out);
case ViewMode::BoxPlot: return render_boxplot(out, cfg, size);
case ViewMode::Stem: return render_stem(out, cfg, size);
case ViewMode::ErrorBars: return render_errorbars(out, cfg, size);
case ViewMode::Pie: return render_pie(out, cfg, false, size, clicked_row_out);
case ViewMode::Donut: return render_pie(out, cfg, true, size, clicked_row_out);
case ViewMode::Funnel: return render_funnel(out, cfg, size, clicked_row_out);
case ViewMode::Waterfall: return render_waterfall(out, cfg, size);
case ViewMode::KPI: return render_kpi_single(out, cfg);
case ViewMode::KPIGrid: return render_kpi_grid(out, cfg);
case ViewMode::Candlestick: return render_candlestick(out, cfg, size);
case ViewMode::Radar: return render_radar(out, cfg, size);
}
return false;
}
} // namespace viz
@@ -1,40 +0,0 @@
// viz: dispatcher de visualizaciones ImPlot sobre StageOutput.
// Cada modo elige automaticamente las columnas relevantes (primera categorica,
// primera o varias numericas) salvo override desde UI.
#pragma once
#include "data_table_logic.h"
#include "imgui.h"
#include <vector>
namespace viz {
// Render principal. Devuelve true si renderiza el modo solicitado, false si
// no se cumplen pre-condiciones (faltan cols numericas/categoricas etc.).
//
// `size`: ImVec2(-1,-1) usa todo el espacio disponible.
// `out`: output del stage activo (headers, types, cells flat row-major).
// `clicked_row_out`: si != nullptr, el render escribira el indice de row del
// `StageOutput` clicado por user. -1 si no hubo click drillable. Fase 10
// (issue 0079): habilitado para bar/column/pie/donut/funnel/scatter/bubble/
// heatmap. Resto de modos: no hit-test, queda en -1.
bool render(const data_table::StageOutput& out,
data_table::ViewMode mode,
const data_table::ViewConfig& cfg,
ImVec2 size = ImVec2(-1, -1),
int* clicked_row_out = nullptr);
// Helper expuesto: encuentra primera col numerica. -1 si ninguna.
int first_numeric_col(const data_table::StageOutput& out);
// Helper: primera col categorica (String/Date/Bool/Json o Int con muchos
// uniques bajos — heuristica). -1 si ninguna.
int first_category_col(const data_table::StageOutput& out);
// Helper: extrae columna como vector<double>. Cells no parseables -> NaN.
std::vector<double> extract_numeric(const data_table::StageOutput& out, int col);
// Helper: extrae columna como vector<string> (categorias).
std::vector<std::string> extract_category(const data_table::StageOutput& out, int col);
} // namespace viz
Submodule cpp/apps/runtime_test deleted from 49a9f3273d
-5
View File
@@ -1,5 +0,0 @@
shaders_lab
shaders_lab.exe
build/
*.zip
operations.db*
-35
View File
@@ -1,35 +0,0 @@
add_imgui_app(shaders_lab
main.cpp
compiler.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/gl_loader.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/gl_shader.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/gl_framebuffer.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/fullscreen_quad.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/shader_canvas.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/uniform_parser.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/uniform_panel.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_catalog.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_compile.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_uniforms.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_panel.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_node_editor.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_palette.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_node_previews.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/shaderlab_db.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/code_to_generator.cpp
# Primitivos UI usados por el modal Save-as-generator.
${CMAKE_SOURCE_DIR}/functions/core/modal_dialog.cpp
${CMAKE_SOURCE_DIR}/functions/core/text_input.cpp
${CMAKE_SOURCE_DIR}/functions/core/button.cpp
# fps_overlay, panel_menu, layouts_menu, app_menubar, layout_storage ya
# viven en fn_framework.
)
target_include_directories(shaders_lab PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(shaders_lab PRIVATE imgui_node_editor SQLite::SQLite3)
if(WIN32)
# GUI app: sin consola al lanzar (subsystem:windows / -mwindows)
set_target_properties(shaders_lab PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
@@ -1,231 +0,0 @@
# shaders_lab — proximos pasos: ventana sin decoraciones del SO + botones min/max/close en la MainMenuBar
## Motivacion
Hoy la ventana lleva la titlebar nativa de Windows/Linux ademas de la
MainMenuBar de ImGui (View). Son dos barras consumiendo ~60 px verticales.
Queremos:
1. **Recuperar ese espacio** quitando la titlebar del SO.
2. **Integrar los botones min / max / close** en la MainMenuBar de ImGui
(la que renderiza `panel_menu_cpp_core`), alineados a la derecha.
3. Resultado: una sola barra superior compacta con menus + botones de
ventana, igual que VSCode/Spotify/etc.
Aplicable a **todas las apps** del registry, no solo shaders_lab — debe
materializarse como funcion(es) reusables en `cpp/functions/core/`.
---
## Que hace falta
### 1. Crear la ventana sin decoraciones
Una linea en `cpp/framework/app_base.cpp` (donde se crea la GLFW window,
linea ~37):
```cpp
glfwWindowHint(GLFW_DECORATED, GLFW_FALSE);
```
Detras de un nuevo flag `AppConfig::borderless = false` para que las apps
existentes sigan iguales por defecto.
### 2. Dibujar la titlebar custom
Hoy `panel_menu_cpp_core` ya pinta una `BeginMainMenuBar()` con el menu
"View". Ampliamos esa misma barra con:
- Titulo de la app a la izquierda (antes del primer menu) — opcional.
- Espacio para drag en el centro (la propia barra ya es draggeable
porque ImGui detecta clicks fuera de los items).
- Tres botones a la derecha alineados con `SameLine` + offset:
min (—), max (□ / ⧉ segun estado), close (✕).
Glifos: ImGui::ImDrawList con lineas/rect, sin necesidad de fuente
de iconos. O symbols Unicode si la fuente los soporta.
### 3. Lo que el SO te daba gratis y hay que reimplementar
**Drag de la ventana**
```cpp
// Detectar mouse-down en la titlebar (no sobre items):
if (!ImGui::IsAnyItemHovered() && ImGui::IsMouseDragging(0)) {
double cx, cy; glfwGetCursorPos(window, &cx, &cy);
int wx, wy; glfwGetWindowPos(window, &wx, &wy);
// guardar offset al iniciar drag, aplicar glfwSetWindowPos cada frame
}
```
**Doble-click en titlebar → toggle maximize**
```cpp
if (ImGui::IsMouseDoubleClicked(0) && !ImGui::IsAnyItemHovered()) {
if (glfwGetWindowAttrib(window, GLFW_MAXIMIZED))
glfwRestoreWindow(window);
else
glfwMaximizeWindow(window);
}
```
**Botones**
```cpp
glfwIconifyWindow(window); // min
glfwMaximizeWindow(window) / glfwRestoreWindow(window); // max toggle
glfwSetWindowShouldClose(window, true); // close
```
**Resize desde bordes** — la parte fea. Sin decoraciones GLFW no expone
hit-test de bordes. Hay que detectar mouse en franjas de ~6 px en
los 8 lados, cambiar cursor (`glfwSetCursor` con `GLFW_HRESIZE_CURSOR`
etc.), y arrastrar reposicionando+resizing manualmente.
---
## Lo que se pierde
| Comportamiento | Recuperable |
|---------------------------------------------------------|----------------------------------------------------------------------------|
| Snap zones de Windows (Win+flecha, drag al borde) | Solo con codigo nativo Win32 (`WM_NCHITTEST`) — GLFW no lo expone |
| Aero shake, sombras nativas, animacion de minimizar | No facilmente |
| Snap del WM en Linux (i3/sway/KDE) | Igual |
| Bugs Wayland posicionando ventanas borderless | Si Linux es WSLg/X11 sin problema; en Wayland nativo verificar primero |
| Multi-viewport ImGui (`ConfigFlags_ViewportsEnable`) | Cada ventana secundaria tambien sin titlebar → custom en todas. Mas curro |
| Touch / accesibilidad (lectores de pantalla) | Marginal para nuestro caso |
---
## Plan de implementacion
### Fase 1 — funcion reusable + flag en app_base
**Funcion nueva**: `custom_titlebar_cpp_core` (componente, pure desde el
punto de vista del registry — solo dibuja UI; los efectos GLFW se aplican
fuera o se pasan como callbacks). Idealmente fusionada con
`panel_menu_cpp_core` o coordinada con ella para que el menu y los botones
vivan en la **misma** MainMenuBar.
Opcion A (mejor): extender `panel_menu` con parametros opcionales para
los botones del SO:
```cpp
struct WindowControls {
GLFWwindow* window;
bool show_min = true;
bool show_max = true;
bool show_close = true;
};
bool panel_menu(const char* menu_label,
const PanelToggle* items, std::size_t count,
const WindowControls* controls = nullptr); // opcional
```
Pero esto crea acople de `core` con GLFW. Mejor opcion B:
Opcion B (limpia): funcion separada `custom_titlebar_cpp_core` que se
llama **dentro** del menu existente (despues de los menus, antes del
EndMainMenuBar) usando `ImGui::SameLine` con offset al borde derecho. Y
una funcion auxiliar `window_controls_cpp_core` para los tres botones,
que recibe callbacks (`on_min`, `on_max`, `on_close`) sin saber nada de
GLFW. La app las cablea.
```cpp
namespace fn_ui {
struct WindowButtons {
bool min_clicked = false;
bool max_clicked = false;
bool close_clicked = false;
bool is_maximized = false; // input: pinta el icono correcto
};
// Renderiza tres iconos al borde derecho de la barra activa
// (BeginMainMenuBar o cualquier otro contenedor horizontal).
// Devuelve los flags que clico el usuario.
WindowButtons window_controls(bool is_maximized);
// Drag handler: llamar cada frame cuando mouse esta sobre la barra.
// Devuelve delta a aplicar a la posicion de ventana.
// (signature por afinar)
struct WindowDrag { int dx, dy; bool dragging; };
WindowDrag titlebar_drag_handler();
} // namespace fn_ui
```
La app conecta:
```cpp
auto wb = fn_ui::window_controls(glfwGetWindowAttrib(window, GLFW_MAXIMIZED));
if (wb.min_clicked) glfwIconifyWindow(window);
if (wb.max_clicked) { /* toggle */ }
if (wb.close_clicked) glfwSetWindowShouldClose(window, true);
```
Asi `core` no toca GLFW; `framework/app_base` o cada app cablean lo
nativo.
**Cambio en app_base**:
```cpp
struct AppConfig {
// ...existing...
bool borderless = false; // true → GLFW_DECORATED=false
};
// en run_app():
if (config.borderless) glfwWindowHint(GLFW_DECORATED, GLFW_FALSE);
```
### Fase 2 — integracion en shaders_lab
```cpp
fn::AppConfig cfg;
cfg.borderless = true;
// ...
```
En `render()` despues del `panel_menu("View", ...)`, en la misma barra
(reorganizar para que `panel_menu` no cierre `EndMainMenuBar` y deje
hacer SameLine al borde derecho con `window_controls`).
### Fase 3 (opcional) — resize por bordes
Manejador de hit-test en 8 px alrededor del borde de la ventana.
Cambia cursor con `glfwSetCursor`, en mouse-down inicia resize manual
con `glfwSetWindowSize` + `glfwSetWindowPos`.
### Fase 4 (solo si se nota la perdida) — snap de Windows nativo
`WM_NCHITTEST` via HWND. `#ifdef _WIN32`, `glfwGetWin32Window`,
SetWindowLongPtr para subclassear. Trabajo significativo; postponer
hasta haber medido si la falta de snap molesta de verdad.
---
## Decisiones pendientes para el dia que se haga
1. Resize manual por bordes en v1 o solo arrastre + maximizar.
2. Si hacemos `WM_NCHITTEST` o aceptamos sin snap de Windows.
3. Multi-viewport ImGui: queda off mientras la titlebar sea custom, o se
replica el control en cada secondary window.
4. Forma final del API: `panel_menu` extendido vs `window_controls` aparte
(preferencia actual: aparte, mas limpia).
---
## Mi recomendacion practica
Empezar minimal:
- Borderless ON
- Drag arrastrando la MainMenuBar
- Doble-click maximiza/restaura
- Botones min/max/close al borde derecho
- **Sin resize manual** (la ventana es solo maximizable; util para apps tipo lab/dashboard)
- **Sin WM_NCHITTEST** (sin snap de Windows)
- Multi-viewport off
Eso es 80% del valor con 20% del trabajo. Si despues echamos en falta el
resize manual o el snap, se anaden incremental.
@@ -1,151 +0,0 @@
# shaders_lab — proximos pasos: tipos de datos en las aristas del DAG
## Estado actual
Por cada arista del DAG circula **un solo tipo**: `vec4` (RGBA por pixel).
El DAG no es un grafo de pasadas de render — es una plantilla que se compila
a **un unico fragment shader GLSL 330 core** (ver `cpp/functions/gfx/dag_compile.cpp`).
Cada nodo se compila a `vec4 node_<i>(vec4 a, ..., vec2 uv)`. Las conexiones
del editor son llamadas a funcion dentro del mismo `main()`.
Datos accesibles dentro de cualquier nodo, **sin pasar por aristas**:
- `u_time`, `u_resolution`, `u_mouse` (preamble de `gl_shader`)
- `u_params[64]` (vec4 array global con todos los parametros del DAG)
- `u_preview_target` (int, para thumbnails per-nodo)
- `uv` (pasado explicitamente como ultimo argumento)
Tipos de nodo (`DagKind` en `cpp/functions/gfx/dag_types.h`):
| Kind | num_inputs | Recibe | Produce |
|--------|-----------:|------------------------------|------------------------|
| Gen | 0 | `uv`, `u_params` | `vec4` generado |
| Op | 1 | `vec4 a, vec2 uv` | `vec4` transformado |
| Blend | 2 | `vec4 a, vec4 b, vec2 uv` | `vec4` combinado |
| Output | 1 | (sumidero) | `fragColor` directo |
---
## Tier 0 — Pins tipados (otros tipos GLSL escalares/vectoriales)
Mismo modelo single-pass. Solo hay que **declarar tipo por pin** y ajustar
el fallback de input vacio en `dag_compile.cpp:75-89`.
| Tipo | Para que | Coste |
|--------|--------------------------------------------------|----------|
| float | mascaras, alpha, heightmaps, distance fields | trivial |
| vec2 | UVs deformadas, gradientes, flow fields | trivial |
| vec3 | normales, posiciones, color sin alpha | trivial |
| mat2/3 | warps, rotaciones, transforms 2D | trivial |
Desbloquea **Op de displacement**: nodo que toma `vec2` (offset) + `vec4`
(textura) y devuelve `vec4` muestreado con offset.
Cambios necesarios:
- `DagNodeDef` declara tipo por pin de entrada y por salida
- `dag_compile` genera `node_<i>` con esas firmas
- `dag_node_editor` pinta colores de pin distintos y rechaza conexiones incompatibles
---
## Tier 1 — Imagenes rasterizadas (texturas)
Sigue siendo single-pass. Anade un **Gen `image_load`** con `uniform sampler2D`
y emite `texture(u_img_<i>, uv)` como `vec4`. Lo mismo aplica a:
- **Video**`sampler2D` re-subido cada frame
- **Webcam** → idem
- **Audio FFT**`sampler1D` (espectro) o `sampler2D` con historial
Coste: **medio**. Hace falta gestor de texturas (slot binding, hot-reload,
resize). La primitiva `Framebuffer` ya existe (`gl_framebuffer.h`) y
`gl_shader` ya gestiona uniforms — solo falta el camino de carga PNG/JPG → GL.
---
## Tier 2 — Multi-pass (rompe el modelo de un solo shader)
**Salto arquitectonico**. Hay operaciones imposibles en un solo pase porque
cada nodo solo "ve" su propio pixel:
- Blur real con kernel grande (lee vecinos)
- Downsample / mipmap / piramides
- FFT, convoluciones, dilations
- Bloom, glow, SSAO
- Reaction-diffusion, fluidos, feedback (frame anterior)
- Cualquier filtro que requiera la imagen ya rasterizada
Una arista deja de ser `vec4` y pasa a ser **texture handle (FBO completo)**.
Cada nodo se compila a su propio fragment shader, renderiza a su FBO, y el
siguiente lo muestrea como `sampler2D`.
`dag_compile` cambia de naturaleza: pasa de "compilar a un main()" a
**planificar passes** (orden topologico, asignar FBOs con pool reusable,
ejecutar en orden).
**Modos coexisten**: cada nodo declara `mode: inline` (se inlinea como hoy)
o `mode: pass` (FBO propio). En cruces `pass→inline` se muestrea el FBO; en
`inline→inline` sigue siendo llamada a funcion. Mejor de los dos mundos.
Coste: **alto** pero la base ya esta (`Framebuffer`, `fullscreen_quad`).
Es trabajo de arquitectura, no de OpenGL.
---
## Tier 3 — SDF (Signed Distance Fields)
Sub-dominio aparte. Sigue siendo single-pass. **Mucho retorno por poco
codigo** — solo necesita pins de tipo `float` (Tier 0) + nuevos nodos.
- Aristas llevan `float` (distancia con signo)
- Gens: `sdf_sphere`, `sdf_box`, `sdf_torus`, `sdf_plane`
- Ops: `sdf_smooth_union`, `sdf_intersect`, `sdf_subtract`, `sdf_round`, `sdf_displace`
- Terminator: `sdf_raymarch` toma SDF + material → `vec4` (raymarching dentro del fragment)
Te da **3D real (esferas, mezclas organicas, fractales tipo Mandelbulb) sin
mallas, vertices ni camara fuera del shader**. Es el truco de la mitad de Shadertoy.
---
## Tier 4 — Geometria 3D real
Aqui ya **no es solo un fragment shader**. Necesita:
- Vertex buffers (`GL_ARRAY_BUFFER`) con posiciones, normales, UVs
- Vertex shader + fragment shader emparejados
- Depth buffer
- Matrices `model/view/projection`, camara
- Posiblemente indices, instancing, geometry/tess shaders
El DAG cambia de naturaleza: dos sub-grafos distintos.
1. **Grafo de geometria** (mallas, transforms, materiales, luces)
2. **Grafo de imagen** (post-process del render final)
Forma realista de integrarlo: un nodo "Render 3D Scene" (con mini-DAG
interno de mallas/camara/luces) **produce una textura** que entra en el
grafo 2D existente como cualquier otro `vec4`.
Coste: **muy alto**. Practicamente otra app dentro de la app.
---
## Tier 5 — Compute / particulas / simulaciones
Compute shaders (`GL_COMPUTE_SHADER`), SSBOs, transform feedback. Para
sistemas de particulas con miles/millones de elementos, simulaciones
fisicas, reaction-diffusion masivo, etc.
Coste muy alto, valor medio salvo que el objetivo del lab cambie.
---
## Recomendacion de orden
1. **Pins tipados** (Tier 0) — desbloquea displacement, mascaras, base para SDFs.
2. **Texturas** (Tier 1) — `image_load`, `video`, `webcam`, `audio_fft`.
3. **SDF + raymarch** (Tier 3) — maximo retorno por linea de codigo.
4. **Multi-pass** (Tier 2) — el salto arquitectonico. Permite blur real, bloom, feedback.
5. **Geometria 3D** (Tier 4) — solo si el caso de uso lo justifica; Tier 3 ya cubre mucho 3D estetico.
Tier 2 es el cruce de caminos: hasta ahi puedes ir extendiendo el modelo
actual; a partir de ahi toca redisenar `dag_compile` como planificador de passes.
-825
View File
@@ -1,825 +0,0 @@
# Shader Playground — MVP Spec
> Editor web de shaders GLSL con:
> - **Auto-UI** generada a partir de anotaciones en `uniform`s.
> - **Integración con Claude API** para generar y modificar shaders desde chat.
> - **Registro mínimo de funciones** reutilizables que el LLM puede consultar e inyectar.
> - **Sistema de sidebars modulares** estilo apps de VJing: canvas central protagonista, paneles acoplables/ocultables a los lados.
> - **Output fullscreen** para sesiones de VJing.
>
> Pensado para completarse en un finde (fase A sábado + fase B domingo).
---
## 0. Filosofía y no-objetivos
### Objetivos del MVP
- Escribir GLSL en un editor web, ver el resultado en vivo, con el canvas como protagonista visual.
- Declarar `uniform`s anotados → panel de controles se genera solo (sliders, color pickers, xy-pads, knobs).
- Chat lateral con Claude que genera shaders, los modifica, y **usa el fn-registry como herramienta** para reutilizar código existente.
- Biblioteca de funciones GLSL con búsqueda, tal que el usuario pueda "guardar este fbm" o "guardar este efecto de nube" y reutilizarlo en futuros shaders.
- Guardar/cargar shaders y funciones GLSL en `localStorage`.
- Modo fullscreen del canvas para usar en sesiones reales de VJ.
### NO objetivos (explícitamente fuera del MVP)
- ❌ Backend propio / base de datos / multi-usuario.
- ❌ Visualizaciones matemáticas auxiliares (FFT, campos, derivadas).
- ❌ MIDI / OSC / audio FFT / Syphon / Spout / NDI.
- ❌ Multi-pass / buffers encadenados tipo Shadertoy.
- ❌ Vertex shader custom (solo fullscreen quad fijo).
- ❌ Compute shaders.
- ❌ Fine-tuning del LLM, RAG elaborado, embeddings.
- ❌ Categorías/taxonomía compleja del registry (flat namespace con tags es suficiente).
- ❌ Múltiples shaders simultáneos con crossfade / capas estilo Photoshop.
- ❌ Sidebars flotantes arrastrables tipo Ableton/Resolume (siempre acoplados a los bordes).
Si el MVP se usa de verdad durante un mes, las features de arriba entran en futuras iteraciones **una a una**.
---
## 1. Stack técnico
- **Package manager / runtime dev:** Bun.
- **Build:** Vite.
- **UI:** React 18 + TypeScript strict.
- **Estilos:** Tailwind + shadcn/ui.
- **Iconos:** lucide-react.
- **Estado:** Zustand.
- **Editor de código:** CodeMirror 6 (paquetes `@codemirror/state`, `@codemirror/view`, `@codemirror/legacy-modes` para GLSL).
- **Renderer:** WebGL2 directo (sin regl ni Three.js). Wrapper propio minimal en `src/renderer/`.
- **Layout:** CSS Grid + `react-resizable-panels` para los sidebars acoplados.
- **Color picker:** `react-colorful`.
- **LLM:** Claude API (`@anthropic-ai/sdk`) usando `claude-opus-4-7` con streaming.
- **Persistencia:** `localStorage` directo, envuelto en módulo fino.
### Por qué WebGL2 puro y no regl
- Vamos a hacer cosas específicas (hot-swap de programas, introspección de uniforms activos, manejo fino de errores de compilación con números de línea) que regl abstrae de formas que más tarde querríamos revertir.
- Aprender la API te deja preparado para WebGPU/wgpu en v2.
- El wrapper que necesitamos son ~200 líneas de TypeScript. Aceptable.
### Estructura de carpetas
```
src/
editor/ # CodeMirror wrapper y modo GLSL
renderer/ # WebGL2 wrapper, compile pipeline, fullscreen quad
parser/ # Extracción de uniforms desde GLSL source
registry/ # fn-registry: CRUD, búsqueda, inyección
llm/ # Cliente de Claude + tool definitions + prompt templates
ui/
layout/ # Icon rail, sidebar containers, canvas stage
sidebars/ # CodeSidebar, ControlsSidebar, AgentSidebar, RegistrySidebar
controls/ # Slider, ColorPicker, XYPad, Knob, Toggle (widgets individuales)
components/ # shadcn/ui imports
store/ # Zustand stores (uno dedicado a layout)
storage/ # localStorage wrapper + schema
seed/ # Shaders y funciones de ejemplo que se cargan la primera vez
App.tsx
main.tsx
```
---
## 2. Layout de la aplicación (sistema de sidebars)
### Principios
- **El canvas del preview es siempre el protagonista visual.** Ocupa el área central y nunca se reduce a menos de ~60% del viewport salvo en layouts atípicos. Nada de mandarlo a un rincón.
- **Dos sidebars visibles a la vez** como configuración por defecto: uno a la izquierda (típicamente el Code), uno a la derecha (típicamente los Controls). El resto se invocan cuando hacen falta y reemplazan al que esté en ese lado.
- **Sidebars acoplados a los bordes**, no flotantes ni arrastrables. Simplicidad > flexibilidad en MVP.
- **Ancho de sidebar arrastrable** (min 240px, max ~600px), persistido por sidebar.
- **Toggle suave** (show/hide con animación corta, 150ms).
### Zonas y componentes
```
┌──┬────────────────────┬────────────────────────┬────────────────────┐
│ │ │ │ │
│ │ │ │ │
│I │ Left sidebar │ Canvas (preview) │ Right sidebar │
│c │ (CODE o │ WebGL2 fullscreen │ (CONTROLS o │
│o │ REGISTRY) │ quad │ AGENT) │
│n │ │ │ │
│ │ │ │ │
│R │ │ │ │
│a │ │ │ │
│i │ │ │ │
│l │ │ │ │
│ │ │ │ │
└──┴────────────────────┴────────────────────────┴────────────────────┘
```
### Icon rail (columna vertical fija, siempre visible)
Ancho ~48px en el borde izquierdo. Iconos verticales con `lucide-react`:
- 📄 **Code** (ícono `FileCode2`) — toggle del CodeSidebar.
- 🎛️ **Controls** (ícono `Sliders`) — toggle del ControlsSidebar.
- 💬 **Agent** (ícono `Sparkles` o `MessageSquare`) — toggle del AgentSidebar.
- 📚 **Registry** (ícono `Library` o `BookOpen`) — toggle del RegistrySidebar.
- ─── separador ───
- 💾 **Shaders** (ícono `Save`) — abre el panel de shaders guardados (también es un sidebar, en el lado opuesto al que esté libre).
- ⚙️ **Settings** (ícono `Settings`) — abre modal de settings (API key, modelo, tema).
- ⏏️ **Fullscreen** (ícono `Maximize2`) — entra en modo fullscreen VJ.
Cada botón del rail muestra un indicador visual si su sidebar está activo (punto de color al lado del icono, o fondo resaltado).
### Reglas de apertura de sidebars
Cada sidebar tiene un "lado preferido":
- `CODE` → izquierda (preferente).
- `CONTROLS` → derecha (preferente).
- `AGENT` → derecha (preferente).
- `REGISTRY` → izquierda (preferente).
- `SHADERS` → izquierda (preferente).
Al pulsar el icono:
1. Si ese sidebar ya está abierto → cerrarlo.
2. Si no está abierto → abrirlo en su lado preferido, sustituyendo lo que hubiera en ese lado.
3. Modificador `Alt + click` sobre el icono → abrirlo en el lado opuesto (forzar).
Solo puede haber **un sidebar por lado**. No se apilan, no hay tabs superpuestos en el MVP.
### Estado del layout en Zustand
```ts
type SidebarId = 'code' | 'controls' | 'agent' | 'registry' | 'shaders';
type Side = 'left' | 'right';
interface LayoutState {
sidebars: {
left: SidebarId | null;
right: SidebarId | null;
};
widths: Record<Side, number>; // px, persistido
fullscreen: boolean;
toggle: (id: SidebarId, opts?: { forceSide?: Side }) => void;
close: (side: Side) => void;
setWidth: (side: Side, width: number) => void;
enterFullscreen: () => void;
exitFullscreen: () => void;
}
```
### Layout por defecto al primer arranque
```
Left: CODE
Right: CONTROLS
```
Esto es el "layout trabajo": editor + controles en vivo alrededor del canvas. Persiste en localStorage.
### Modo fullscreen (modo VJ)
- Icon rail, sidebars y topbar desaparecen completamente.
- Canvas ocupa el 100% del viewport.
- Atajos siguen funcionando: `F` o `Esc` salen.
- Teclas `1..9` cargan shaders guardados por índice (útil para directo).
- **Overlay botón transparente** en esquina inferior derecha (icono `Maximize2` inverso, solo visible al mover el ratón en los últimos ~2 segundos, fade-out después). Click → salida de fullscreen. Esto es el "como en Resolume": para cuando estás tocando y quieres volver sin buscar tecla.
- Para ajustar uniforms en fullscreen sin salir, el camino es salir con `Esc`, ajustar, y volver a `F`. El MVP no tiene edge panels translúcidos — eso se evaluará en v2 tras uso real.
### Topbar (fuera de fullscreen)
Encima del canvas, arriba del todo, ~40px de alto:
- Izq: nombre del shader actual (editable en línea con doble click).
- Centro: botones Play / Pause / Reset time.
- Der: indicador de compile (verde OK / rojo con `error on line X`), botón nuevo, selector de shader (dropdown).
---
## 3. Contenido de cada sidebar
### CODE sidebar
- Header: título "Code" + indicador de modo actual (sidebar / overlay).
- Body: CodeMirror 6 con GLSL syntax highlight, números de línea, error underlining.
- Footer: info line con bytes, líneas, último compile time, estado (OK / Error línea N).
**Dos modos de visualización, elegibles en Settings:**
1. **Sidebar mode** (default): el editor vive acoplado al borde izquierdo, como cualquier otro sidebar. Coexiste con el preview y los controles. Es el modo "trabajo".
2. **Overlay mode** (estilo apps VJ): al activar CODE, en lugar de abrir un sidebar, aparece un **modal semitransparente** (70% opacidad, fondo oscurecido) flotante sobre el canvas. El canvas sigue renderizando por debajo. `Esc` o click fuera cierra el modal. Es el modo "live coding / VJ" donde el canvas al máximo es lo importante y el código es una ventana que aparece y desaparece.
La elección vive en el modal de Settings (ver §9). El usuario puede cambiar entre modos en cualquier momento. El comportamiento del icono "Code" en el rail se adapta automáticamente: en sidebar mode abre el sidebar, en overlay mode abre el modal.
**Atajo `Cmd/Ctrl + /`** — independientemente del modo configurado, abre el editor en overlay temporalmente. Útil para un vistazo rápido sin cambiar preferencia.
### CONTROLS sidebar
- Header: "Controls" + botón "Reset to defaults" (devuelve todos los uniforms a su `default` del shader actual).
- Body: lista vertical de widgets autogenerados desde los uniforms anotados. Cada uniform es una "card" con:
- Nombre del uniform.
- Widget (Slider / ColorPicker / XYPad / Knob / Toggle / Slider2D).
- Valor actual formateado numéricamente.
- Footer: contador "N uniforms detected".
- Scroll vertical si no caben.
Si el shader no tiene uniforms anotados: mensaje placeholder *"Declare uniforms with `// @slider ...` annotations to see controls here."* con un link "See annotation format" que abre un popover con ejemplos.
### AGENT sidebar
- Header: "Agent" + selector de modelo (dropdown: Opus/Sonnet/Haiku) + botón "Clear conversation".
- Body: lista de mensajes con markdown rendering, bloques de código GLSL con highlight, colapsables para `tool_use` y `tool_result` ("🔧 Searched registry: 3 results").
- Botón "Apply this shader" junto a bloques de código en respuestas del LLM (ver §7).
- Below body: chips con prompts de demostración.
- Footer: textarea de input multilínea, `Cmd/Ctrl + Enter` envía, `Shift + Enter` nueva línea.
### REGISTRY sidebar
- Header: "Functions" + input de búsqueda (filtra en vivo por name/description/tags).
- Body: lista de funciones registradas. Cada item:
- Nombre + signature.
- Tags como pills (color distinto por tag).
- Descripción corta (2 líneas max, truncada).
- Acciones hover: "Insert into current shader" (añade al `@registry_inject_begin/end` markers), "View code" (expande inline), "Edit", "Delete".
- Footer: botón "+ New function" (abre modal de creación), "Import/Export JSON".
### SHADERS sidebar
- Header: "Saved shaders" + botón "+ New".
- Body: lista de shaders guardados. Cada item:
- Thumbnail pequeño (64x36 px) generado rasterizando el shader en un canvas offscreen al guardar.
- Nombre + fecha de última edición.
- Acciones hover: "Load", "Rename", "Duplicate", "Delete", "Export".
- Search por nombre en la cabecera.
Los thumbnails son una mejora visual importante para VJing (reconocimiento instantáneo). Si no da tiempo, fallback a iconos/gradientes generados deterministicamente desde el nombre (tipo GitHub identicons).
---
## 4. Invocación de sidebars
### Canal principal: icon rail
El **icon rail vertical permanente** en el borde izquierdo es el único camino de descubrimiento y uso habitual. Siempre visible (excepto en fullscreen VJ). Click en el icono → toggle del sidebar. Alt+click → abrir en lado opuesto al preferido.
El rail es la fuente de verdad del layout: cualquier usuario, sin leer documentación, sabe qué sidebars existen y puede abrirlos con un click.
### Atajos de teclado (para usuarios avanzados)
Existen pero no son el canal principal — duplican funcionalidad del rail para quien quiera manos en el teclado:
- `F1..F5` — toggle de cada sidebar (ver §11 para la tabla completa).
- `Cmd/Ctrl + B` — colapsar ambos sidebars (canvas máximo sin fullscreen).
- `F` — fullscreen VJ.
- `Esc` — cerrar lo que esté abierto en cascada.
### Modo VJ: botón flotante para salir de fullscreen
En modo fullscreen, al mover el ratón aparece un único botón flotante translúcido en la esquina inferior derecha para salir. Fade-out tras 2s. No hay más invocaciones flotantes — todo lo demás via `Esc` o teclas de atajo.
---
## 5. Renderer (WebGL2 puro)
### Fullscreen quad fijo
Vertex shader no editable:
```glsl
#version 300 es
in vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}
```
Geometría: dos triángulos cubriendo `[-1,1]²`.
### Fragment shader — lo escribe el usuario
El wrapper antepone automáticamente:
```glsl
#version 300 es
precision highp float;
out vec4 fragColor;
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
```
Estos tres uniforms siempre están disponibles, el parser los ignora (no aparecen en controls).
### Render loop
- `requestAnimationFrame`, `performance.now()``u_time` en segundos.
- Play/pause congela/descongela `u_time`.
- Reset pone `u_time = 0`.
- Canvas se redimensiona vía `ResizeObserver` al tamaño del stage central (cambia cuando se abren/cierran sidebars).
### Compile pipeline
- Al cambiar source: debounce 250 ms.
- `gl.createShader``gl.shaderSource``gl.compileShader`.
- Si `COMPILE_STATUS` es false: `gl.getShaderInfoLog()`, parsear línea (formato `ERROR: 0:<line>: <msg>`), propagar al editor.
- Si compila: link program, swap atómico con el anterior, delete del anterior.
- Si el programa nuevo falla: mantener el anterior, canvas NUNCA se queda en negro por un error.
- Introspección post-link: `gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS)` para validar que el parser encontró lo mismo que GLSL realmente expone. Discrepancias → warning en consola (no error).
### Wrapper API
```ts
interface Renderer {
compile(source: string): Promise<CompileResult>;
setUniform(name: string, value: number | number[] | boolean): void;
setPlaying(playing: boolean): void;
resetTime(): void;
resize(w: number, h: number): void;
snapshot(w: number, h: number): Promise<ImageBitmap>; // para thumbnails
dispose(): void;
}
type CompileResult =
| { ok: true; activeUniforms: string[] }
| { ok: false; line: number; message: string };
```
---
## 6. Parser de uniforms
### Formato de anotación
```glsl
uniform float u_speed; // @slider min=0 max=5 default=1
uniform float u_freq; // @slider min=0.1 max=100 default=10 log=true
uniform vec3 u_colorA; // @color default=0.1,0.2,0.5
uniform vec4 u_tint; // @color default=1,0.5,0,1
uniform vec2 u_origin; // @xy min=-1 max=1 default=0,0
uniform vec2 u_offset; // @slider2d min=-10,-10 max=10,10 default=0,0
uniform float u_angle; // @knob min=0 max=6.283 default=0
uniform int u_iter; // @slider min=1 max=50 default=10 step=1
uniform bool u_debug; // @toggle default=false
```
### Algoritmo (regex, suficiente para MVP)
Para cada línea:
1. Match `^\s*uniform\s+(\w+)\s+(\w+)\s*;\s*(?:\/\/\s*@(\w+)(.*))?$`
2. Grupo 1: tipo GLSL. Grupo 2: nombre. Grupo 3: widget kind (opcional). Grupo 4: resto de props.
3. Si no hay widget kind, usar defaults:
- `float``slider(min=0, max=1, default=0)`
- `vec2``xy(min=0,0 max=1,1 default=0.5,0.5)`
- `vec3``color(default=1,1,1)`
- `vec4``color(default=1,1,1,1)`
- `int``slider(step=1, min=0, max=10, default=0)`
- `bool``toggle(default=false)`
4. Parse props `key=value` separados por whitespace. Números: `parseFloat`. Vectores: split por `,`. Bools: `"true"/"false"`.
5. Ignorar uniforms con nombre en `{u_resolution, u_time, u_mouse}` (reservados).
6. Ignorar uniforms cuyo tipo sea `sampler2D` (no soportados, warning en consola).
### Tipos TS
```ts
type GLSLType = 'float' | 'vec2' | 'vec3' | 'vec4' | 'int' | 'bool';
type WidgetKind = 'slider' | 'slider2d' | 'color' | 'xy' | 'knob' | 'toggle';
interface UniformDescriptor {
name: string;
glslType: GLSLType;
widget: WidgetKind;
props: Record<string, number | number[] | boolean>;
defaultValue: number | number[] | boolean;
}
type ParseResult = { uniforms: UniformDescriptor[]; warnings: string[] };
```
### Tests (Vitest, en `src/parser/parser.test.ts`)
1. Uniform sin anotación → defaults por tipo.
2. `@slider min=0 max=10 default=5`.
3. `@color default=1,0,0` sobre `vec3`.
4. `@xy default=0.5,0.5` sobre `vec2`.
5. Uniforms reservados ignorados.
6. Comentario malformado → fallback a defaults, warning.
7. Múltiples uniforms en el mismo source, orden preservado.
8. Comentarios de bloque `/* ... */` no interfieren.
---
## 7. Widgets de control
Todos reciben `{value, onChange, descriptor}`, **controlados**, sin estado interno.
### Slider (float, int)
- shadcn/ui `<Slider>`.
- Label con valor numérico, click para editar.
- Si `log=true`: mapeo logarítmico.
- Si `step` definido: respetarlo.
### ColorPicker (vec3, vec4)
- `react-colorful` `RgbaColorPicker`.
- Almacena internamente como array `[r,g,b]` o `[r,g,b,a]` en `[0,1]`.
### XYPad (vec2 con @xy)
- Cuadrado ~150×150 px.
- Drag con `pointerdown/move/up`.
- Mapea `[0,1]²` del DOM a `[min.x, max.x] × [min.y, max.y]` con Y invertida.
- Valores numéricos debajo.
### Slider2D (vec2 con @slider2d)
- Dos sliders apilados con labels `x` / `y`.
### Knob (float con @knob)
- Círculo SVG con marca.
- Drag vertical u horizontal cambia el valor.
- Visual distinto al slider para que se distinga.
### Toggle (bool)
- shadcn/ui `<Switch>`.
---
## 8. fn-registry (diseñado para ser usado por el LLM)
Biblioteca mínima de funciones GLSL reutilizables, guardadas en `localStorage` y consultables tanto por el usuario (REGISTRY sidebar) como por el LLM (vía tool use).
### Modelo de datos
```ts
interface RegisteredFunction {
id: string; // nanoid
name: string; // nombre GLSL, e.g. "hash12"
signature: string; // "float hash12(vec2 p)"
description: string; // 1-2 frases: qué hace
tags: string[]; // ["noise", "hash"]
body: string; // cuerpo GLSL completo (función entera, firma incluida)
dependencies: string[]; // nombres de otras funciones del registry que usa
createdAt: number;
updatedAt: number;
}
```
### Operaciones
- `list()``RegisteredFunction[]`.
- `search(query: string)``RegisteredFunction[]` (match en name, description, tags).
- `get(name: string)``RegisteredFunction | null`.
- `save(fn: RegisteredFunction)` → upsert.
- `delete(id: string)` → void.
- `resolveDependencies(names: string[])` → devuelve el conjunto cerrado transitivo ordenado topológicamente.
### Inyección en el shader actual
Se usa el patrón de **markers**: el shader tiene un bloque marker, y el renderer — antes de compilar — reemplaza el contenido entre markers con el cuerpo de las funciones declaradas más sus dependencias transitivas.
```glsl
// @registry_inject_begin
// hash12, perlin2d, rotate2d
// @registry_inject_end
void main() { ... }
```
Si el usuario edita dentro del bloque manualmente, se regenera al guardar (con confirmación si hay cambios).
### Semilla inicial (seed)
Cargar ~15 funciones clásicas la primera vez que se abre la app. Mínimo:
- `hash11`, `hash12`, `hash22` (hashes deterministas sin `sin()`).
- `value_noise_2d`, `perlin_noise_2d`, `simplex_noise_2d`.
- `fbm` (fractal brownian motion).
- `rotate2d`.
- `sdf_circle`, `sdf_box`, `sdf_line`.
- `smoothmin`.
- `palette` (Inigo Quilez cosine palette).
- `hsv2rgb`, `rgb2hsv`.
Cada una con `tags` y `description` relevantes para que el LLM pueda buscarlas semánticamente.
### Panel Functions (ya descrito en §3 como REGISTRY sidebar)
---
## 9. Integración con Claude (LLM)
### Configuración
- Usuario pega su API key en un modal de Settings → se guarda en `localStorage` (warning claro: "se guarda en local, no la uses en ordenadores compartidos").
- Selector de modelo: `claude-opus-4-7` (default, mejor calidad), `claude-sonnet-4-6` (más rápido), `claude-haiku-4-5-20251001` (muy rápido, para iteración).
### Cliente
- `@anthropic-ai/sdk` con `dangerouslyAllowBrowser: true`.
- Streaming siempre activo.
- Historial de conversación persistido en `localStorage` (último N mensajes, truncable).
### System prompt (template)
```
You are a creative shader programmer helping the user write WebGL2 fragment shaders for visual art and VJing.
The host environment provides these uniforms automatically — never redeclare them:
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
The target is WebGL2 / GLSL ES 3.00. Use `fragColor` as the output (it's predeclared as `out vec4 fragColor`). The `#version 300 es` directive is prepended automatically — don't include it.
When you declare uniforms the user should be able to tweak, annotate them with a magic comment so the UI generates a control automatically. Supported annotations:
uniform float u_speed; // @slider min=0 max=5 default=1
uniform float u_freq; // @slider min=0.1 max=100 default=10 log=true
uniform vec3 u_color; // @color default=0.1,0.2,0.5
uniform vec4 u_tint; // @color default=1,1,1,1
uniform vec2 u_pos; // @xy min=-1 max=1 default=0,0
uniform float u_angle; // @knob min=0 max=6.283 default=0
uniform bool u_debug; // @toggle default=false
Guidelines:
- Prefer to REUSE functions from the registry when possible. You have tools to search and insert registry functions.
- Keep shaders self-contained and working on first compile.
- Use functional style: pure functions, no side effects inside helpers, compose via explicit parameters.
- When producing a complete shader, annotate uniforms the user is likely to want to tweak live.
- Prefer hash functions that don't rely on `sin()` (use hash12/hash22 from the registry).
- If the user asks for a modification, return the full updated shader via apply_shader, not a diff.
- Keep aspect-ratio correctness in mind: use `(gl_FragCoord.xy - 0.5*u_resolution.xy) / u_resolution.y` for centered, non-stretched coordinates unless a different framing is asked for.
Tools available:
- search_registry(query): find functions by name/description/tags.
- get_function(name): retrieve a function's full body.
- list_registry(): list all available function names and signatures.
- apply_shader(source): replace the user's current shader with this source. Use this when the user explicitly asks you to generate or modify their shader.
- save_function({name, signature, description, tags, body, dependencies}): add a function to the registry.
```
### Tools (Anthropic tool use)
```ts
const tools = [
{
name: 'search_registry',
description: 'Search for reusable GLSL functions in the local registry by name, description, or tags.',
input_schema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
},
{
name: 'get_function',
description: 'Retrieve the full body of a registered function by name.',
input_schema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
},
{
name: 'list_registry',
description: 'List all functions in the registry with their signatures and tags.',
input_schema: { type: 'object', properties: {} },
},
{
name: 'apply_shader',
description: 'Replace the user\'s current fragment shader with new source. Use when the user asks to generate or modify their shader.',
input_schema: { type: 'object', properties: { source: { type: 'string' } }, required: ['source'] },
},
{
name: 'save_function',
description: 'Save a reusable GLSL function to the registry so it can be used in future shaders.',
input_schema: {
type: 'object',
properties: {
name: { type: 'string' },
signature: { type: 'string' },
description: { type: 'string' },
tags: { type: 'array', items: { type: 'string' } },
body: { type: 'string' },
dependencies: { type: 'array', items: { type: 'string' } },
},
required: ['name', 'signature', 'body'],
},
},
];
```
### Loop de tool use
Loop estándar de Anthropic: mandar mensaje con `tools`, si `stop_reason === 'tool_use'` ejecutar las tools, añadir resultados como `tool_result`, volver a llamar al API, repetir hasta `stop_reason === 'end_turn'`.
Las tools son **locales y síncronas**: todas tocan `localStorage` o el store Zustand. No hay red más allá de la llamada al API de Anthropic.
### Confirmación antes de aplicar
- `apply_shader` **no reemplaza silenciosamente** el código actual. Muestra un diff side-by-side y el usuario confirma. Crítico para que el LLM no borre trabajo del usuario.
- `save_function` aplica directamente (es aditivo, no destructivo), pero muestra toast con undo.
### Prompts de demostración (chips en el AgentSidebar)
- "Make a lava lamp shader"
- "Add audio-reactive colors using u_time"
- "Refactor the current shader to use registry functions"
- "Explain what this shader does line by line"
- "Create a kaleidoscope with 8 segments"
---
## 10. Persistencia (localStorage)
### Schema
```ts
interface StorageSchema {
version: 1;
currentShader: string; // id del shader actualmente cargado
shaders: Record<string, {
id: string;
name: string;
source: string;
uniformValues: Record<string, unknown>;
thumbnail?: string; // dataURL pequeño para mostrar en SHADERS sidebar
updatedAt: number;
}>;
functions: Record<string, RegisteredFunction>;
conversations: Array<{
id: string;
messages: Array<{ role: string; content: unknown }>;
updatedAt: number;
}>;
settings: {
apiKey: string | null;
model: string;
theme: 'dark' | 'light';
codeMode: 'sidebar' | 'overlay'; // modo del editor: acoplado o flotante
};
layout: {
sidebars: { left: string | null; right: string | null };
widths: { left: number; right: number };
};
}
```
### Claves
- Una única clave root: `shader-playground:v1`.
- Migraciones: si se encuentra `version < 1` o key vieja, crear backup con sufijo timestamp y regenerar.
- Debounce 500 ms en todas las escrituras.
- API key jamás incluida en exports/imports manuales del usuario.
---
## 11. Atajos de teclado (completos)
### Sidebars
- `F1` — toggle CODE sidebar.
- `F2` — toggle CONTROLS sidebar.
- `F3` — toggle AGENT sidebar.
- `F4` — toggle REGISTRY sidebar.
- `F5` — toggle SHADERS sidebar.
- `Cmd/Ctrl + B` — colapsar ambos sidebars (canvas máximo, no fullscreen).
- `Cmd/Ctrl + /` — abrir CODE como modal overlay sobre canvas.
### Render / modo VJ
- `F` — toggle fullscreen VJ.
- `Esc` — cerrar modal → cerrar sidebars → salir fullscreen (en cascada).
- `Space` (fuera del editor) — play/pause.
- `Cmd/Ctrl + R` — reset time.
- `1..9` (en fullscreen) — cargar shader guardado N.
### Trabajo
- `Cmd/Ctrl + S` — guardar snapshot inmediato.
- `Cmd/Ctrl + Enter` — forzar recompile (desde editor) o enviar mensaje (desde chat).
- `Cmd/Ctrl + K` — focus en chat input (abre AGENT si está cerrado).
---
## 12. Criterios de aceptación
### Fase A — Sábado (core + layout)
- [ ] Editor GLSL funcional con CodeMirror y highlight.
- [ ] WebGL2 renderer con fullscreen quad, hot-recompile con debounce.
- [ ] Error de compilación con línea, canvas mantiene el último válido.
- [ ] Parser de uniforms con todas las anotaciones funcionando.
- [ ] 6 widgets de control conectados.
- [ ] Icon rail visible con 6-7 iconos y toggles funcionales.
- [ ] Los 4 sidebars principales (CODE, CONTROLS, SHADERS, settings modal) funcionan con toggle y ancho redimensionable.
- [ ] Layout default (CODE izq + CONTROLS der) al primer arranque.
- [ ] Setting `codeMode` (sidebar / overlay) funciona: al cambiarlo en Settings, el icono "Code" del rail abre el editor en el modo elegido.
- [ ] Estado del layout persiste en localStorage.
- [ ] Cambiar un slider actualiza el render sin lag.
- [ ] Persistencia: recarga la página y vuelve todo igual, incluidos qué sidebars estaban abiertos.
- [ ] 4 shaders de ejemplo cargan correctamente.
- [ ] Fullscreen funciona con `F` y `Esc`, icon rail y sidebars desaparecen limpiamente.
- [ ] Modo "code overlay" funciona cuando está activado en Settings: el icono "Code" abre un modal flotante sobre el canvas en vez del sidebar.
- [ ] Tests del parser pasan.
**Si la fase A está completa y funciona, ya hay algo usable.** La fase B es aditiva.
### Fase B — Domingo (LLM + registry)
- [ ] REGISTRY sidebar con búsqueda, lista filtrable, acciones "insert / view / edit / delete".
- [ ] Seed inicial de ~15 funciones cargadas al primer arranque.
- [ ] Markers `@registry_inject_begin/end` funcionan, se reemplazan antes de compilar.
- [ ] Modal de Settings para API key de Claude.
- [ ] AGENT sidebar con chat funcional, streaming visible.
- [ ] Tool use funcional (`search_registry`, `get_function`, `list_registry`, `apply_shader`, `save_function`).
- [ ] `apply_shader` muestra diff y pide confirmación.
- [ ] Chips de prompts de demostración.
- [ ] Conversación persiste entre recargas.
### Features opcionales (solo si sobra tiempo)
- [ ] Thumbnails generados en el SHADERS sidebar.
- [ ] Botón overlay flotante en fullscreen para salir (si no, queda `Esc`).
- [ ] Export/import del registry como JSON.
### Criterio global
- [ ] Puedo usar la app para: (1) pedirle a Claude "haz un shader de nubes con double domain warping, guardado como función en el registry", (2) verlo aparecer en el REGISTRY sidebar, (3) abrir un shader nuevo en blanco, (4) pedirle "carga la función de nubes del registry y úsala con una paleta roja-naranja", (5) ajustar los sliders que aparezcan automáticamente, (6) guardarlo, (7) entrar en fullscreen, (8) recargar la página y seguir teniendo todo. **Si este flujo end-to-end no funciona, el MVP no está hecho.**
---
## 13. Orden de implementación
### Sábado (Fase A)
1. Scaffold: Bun init, Vite, React, Tailwind, shadcn CLI, Zustand. Layout con icon rail + stage central + sidebar containers vacíos.
2. WebGL2 wrapper: fullscreen quad, fragment shader hardcoded sólido. Verifica render.
3. CodeMirror con GLSL en CODE sidebar. Source en store. Recompile al cambiar (debounce).
4. Error handling: parse del infoLog, display en editor.
5. Layout store: toggle de sidebars, reglas de lado preferido, persistencia del layout.
6. Parser de uniforms + tests.
7. Store de uniformValues sincronizado con descriptors (diff al cambiar shader).
8. CONTROLS sidebar renderizando widgets autogenerados.
9. Widgets uno a uno: Slider → Toggle → Color → XY → Knob → Slider2D. Cada uno end-to-end.
10. Persistencia localStorage con debounce (shaders + layout + uniformValues).
11. SHADERS sidebar: lista, guardar/cargar/renombrar/duplicar.
12. Shaders de ejemplo en seed.
13. Fullscreen + atajos de teclado + setting `codeMode` con ambas variantes (sidebar y overlay).
### Domingo (Fase B)
14. fn-registry: modelo, CRUD, búsqueda, seed, tests.
15. Markers `@registry_inject_begin/end` y preprocesado antes de compilar.
16. REGISTRY sidebar con búsqueda y acciones.
17. Settings modal con API key.
18. Cliente Claude con streaming (sin tools).
19. AGENT sidebar: chat, markdown rendering, persistencia.
20. Tool definitions y loop de tool use.
21. Diff + confirmación para `apply_shader`.
22. Chips de prompts de demostración.
23. Pulido visual, toasts, mensajes de error amigables.
### Qué sacrificar si algo se alarga (en orden, de menos a más crítico)
1. Thumbnails del SHADERS sidebar (fallback: icon/gradient generado desde el nombre).
2. Botón flotante de salida de fullscreen (queda solo `Esc`).
3. `Slider2D` y `Knob` (los `vec2` usan XY, los `float` normal Slider).
4. Setting `codeMode` — dejar solo modo sidebar; overlay va a v2.
5. Modal de creación de función en registry (solo insert, edit a mano en JSON).
6. Chips de prompts de demostración.
7. Markers `@registry_inject_*` (el LLM pega código directo).
---
## 14. Calidad de código
- TypeScript strict mode, `noImplicitAny`, `strictNullChecks`.
- Módulos `parser/`, `renderer/`, `registry/`, `llm/` son **puros y testables sin DOM ni React**.
- Widgets reciben `value`/`onChange` y son ignorantes del store (el puente se hace en `ControlsSidebar`).
- Render loop NO pasa por React. Subscribe al store de Zustand y lee valores directamente cada frame.
- Cada sidebar es un componente React autocontenido que lee/escribe en su slice del store.
- Vitest para tests del parser y del registry (resolver dependencias transitivas).
- Prettier + ESLint básicos, sin fanatismo.
- Commits en imperativo corto ("Add icon rail", "Wire AGENT sidebar to Claude client").
---
## 15. Lo que NO hay que hacer aunque apetezca
- No añadir audio / MIDI en el MVP.
- No añadir multi-pass "porque es solo un buffer más".
- No refactorizar los widgets a una abstracción genérica antes de tener los 6 implementados.
- No hacer los sidebars flotantes/arrastrables tipo Ableton/Resolume. Siempre acoplados a bordes.
- No permitir más de un sidebar por lado en MVP. Si quieres ver REGISTRY y CONTROLS a la vez, abres uno en cada lado. No tabs apilados.
- No meter un sistema de plugins.
- No añadir LangChain, vector DBs, ni RAG. La tool `search_registry` es un string match simple y es suficiente.
- No hacer backend en Go para persistir "cuando sea". Todo en `localStorage` hasta que duela de verdad.
- No soportar vertex shaders custom.
- No soportar múltiples shaders simultáneos con crossfade. Un shader activo, fullscreen, listo.
Cada una de estas es un día comido y medio MVP menos. Después del MVP y de un mes usándolo de verdad, vuelvo a mirar qué duele y decido.
---
## 16. Referencias útiles
- The Book of Shaders: https://thebookofshaders.com
- Shadertoy: https://www.shadertoy.com
- ISF (precedente de las anotaciones): https://isf.video
- Dave Hoskins "Hash without Sine": https://www.shadertoy.com/view/4djSRW
- Inigo Quilez articles: https://iquilezles.org/articles/
- Resolume / VDMX / KodeLife (referencias de UX de VJing): cómo los sidebars se esconden y la importancia del canvas central.
- Claude API docs: https://docs.claude.com
- `@anthropic-ai/sdk` con navegador: usar `dangerouslyAllowBrowser: true`, warning explícito en la UI sobre la exposición de la API key.
-104
View File
@@ -1,104 +0,0 @@
---
name: shaders_lab
lang: cpp
domain: gfx
description: "Live GLSL shader playground con DAG pipeline. Editor de codigo con compilacion en caliente, panel DAG con paleta de generadores/filtros/output, dos canvas (Code y DAG), parseo de uniforms anotados (// @slider, @color, @xy) que se convierten en controles, persistencia de generators en shaders_lab.db, y guardado/carga de layouts ImGui."
tags: [imgui, opengl, glsl, shaders, dag, live-coding, playground, sqlite]
uses_functions:
# gfx
- gl_loader_cpp_gfx
- gl_shader_cpp_gfx
- gl_framebuffer_cpp_gfx
- fullscreen_quad_cpp_gfx
- shader_canvas_cpp_gfx
- uniform_parser_cpp_gfx
- uniform_panel_cpp_gfx
- dag_catalog_cpp_gfx
- dag_compile_cpp_gfx
- dag_uniforms_cpp_gfx
- dag_panel_cpp_gfx
- dag_node_editor_cpp_gfx
- dag_palette_cpp_gfx
- dag_node_previews_cpp_gfx
- shaderlab_db_cpp_gfx
- code_to_generator_cpp_gfx
# core (modal Save-as-generator)
- modal_dialog_cpp_core
- text_input_cpp_core
- button_cpp_core
uses_types:
- dag_types_cpp_gfx
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/shaders_lab"
repo_url: ""
---
## Arquitectura
App ImGui de live-coding GLSL con dos modos en paralelo:
1. **Code panel** — editor de fragment shader libre. Las anotaciones en
uniforms (`// @slider`, `// @color`, `// @xy`, `// @toggle`) se parsean y
convierten en controles del panel **Controls** que escriben en un
`UniformStore` aplicado al programa cada frame.
2. **DAG panel** — pipeline node-based con catalogo de generadores
(plasma, voronoi, etc.) y filtros (blur, threshold, etc.) que se
compilan a un fragment shader unificado y se renderizan en **Canvas DAG**.
Al guardar un Code shader como "generator" se traduce a un `DagNodeDef` y se
persiste en `shaders_lab.db` (tabla via `shaderlab_db`), apareciendo en la
paleta del DAG junto a los builtins.
## Capas
| Archivo | Responsabilidad |
|---|---|
| `main.cpp` | UI shell, paneles, modal save-as, layouts, AppConfig |
| `compiler.cpp` | `compile_code()`, `compile_dag()`, `mark_code_dirty()` con debounce 250ms |
`main.cpp` mantiene estado global de sesion (g_source, g_pipeline, g_descs,
g_store, g_layouts...) — ImGui retained-mode obliga a que persista entre
frames. Toda la logica pura de compilacion vive en `compiler.cpp` y en las
funciones `dag_compile`, `code_to_generator`, `uniform_parser` del registry.
## Persistencia
- **`shaders_lab.db`** (junto al .exe) — tabla de generators de usuario via
`shaderlab_db_*`, ademas de `imgui_layouts` (creada por `layout_storage`).
- `imgui.ini` y `app_settings.ini` — gestionados por `fn::run_app` en
`<exe_dir>/local_files/`.
## Paneles
| Panel | Atajo | Que muestra |
|---|---|---|
| Code | Ctrl+1 | Editor del fragment shader + boton "Save as generator" |
| DAG Pipeline | Ctrl+2 | Node editor con la pipeline |
| Canvas Code | Ctrl+3 | Render del Code shader |
| Canvas DAG | Ctrl+4 | Render del shader compilado del DAG |
| Controls | Ctrl+5 | Sliders/color pickers de uniforms anotados |
| Functions | Ctrl+6 | Paleta del DAG (generators + filters + output) |
| Generated GLSL | Ctrl+7 | GLSL final del DAG con uniforms baked como const array |
## Build
```bash
# Linux
cd cpp && cmake -B build/linux -S . && cmake --build build/linux --target shaders_lab
# Windows (cross-compile)
cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w64.cmake \
&& cmake --build build/windows --target shaders_lab
```
## Decisiones
- `init_gl_loader = true` (via `fn::run_app` por default cuando se enlaza
con OpenGL) — `shader_canvas`, `gl_shader`, `gl_framebuffer` llaman gl*.
- `viewports = true` — los Canvas se pueden arrastrar fuera del main.
- DAG default: arranca con un nodo "plasma" + "output" si la paleta los
encuentra; persiste el INI con `layout_storage`.
- El boton "Save as generator" valida snake_case, evita colisionar con
builtins, traduce con `code_to_generator`, persiste con `shaderlab_db_save_generator`,
y registra el nodo nuevo en el catalogo en vivo (`dag_register_node`).
-63
View File
@@ -1,63 +0,0 @@
#include "compiler.h"
#include "gfx/shader_canvas.h"
#include "gfx/gl_shader.h"
#include "gfx/uniform_parser.h"
#include "gfx/uniform_panel.h"
#include "gfx/dag_compile.h"
#include "gfx/dag_uniforms.h"
#include <chrono>
#include <string>
#include <vector>
// ── Globals declarados en main.cpp (single source of truth) ─────────────────
extern fn::gfx::ShaderCanvas g_canvas_code;
extern fn::gfx::ShaderCanvas g_canvas_dag;
extern std::string g_source;
extern std::string g_code_err;
extern int g_code_err_line;
extern std::chrono::steady_clock::time_point g_code_last_edit;
extern bool g_code_dirty;
extern std::vector<fn::gfx::UniformDescriptor> g_descs;
extern fn::gfx::UniformStore g_store;
extern std::vector<fn::gfx::DagStep> g_pipeline;
extern std::string g_dag_glsl;
extern std::string g_dag_err;
extern int g_dag_err_line;
namespace shaders_lab {
void compile_code() {
auto r = fn::gfx::compile_fragment(g_source);
if (r.ok) {
g_descs = fn::gfx::parse_uniforms(g_source);
fn::gfx::uniforms_sync(g_store, g_descs);
fn::gfx::canvas_set_program(g_canvas_code, r.program);
g_code_err.clear();
g_code_err_line = -1;
} else {
g_code_err = r.err_msg;
g_code_err_line = r.err_line;
}
}
void compile_dag() {
g_dag_glsl = fn::gfx::compile_dag_to_glsl(g_pipeline);
auto r = fn::gfx::compile_fragment(g_dag_glsl);
if (r.ok) {
fn::gfx::canvas_set_program(g_canvas_dag, r.program);
g_dag_err.clear();
g_dag_err_line = -1;
} else {
g_dag_err = r.err_msg;
g_dag_err_line = r.err_line;
}
}
void mark_code_dirty() {
g_code_last_edit = std::chrono::steady_clock::now();
g_code_dirty = true;
}
} // namespace shaders_lab
-24
View File
@@ -1,24 +0,0 @@
#pragma once
// shaders_lab/compiler — extrae las rutinas impuras de compilacion del shader
// (compile_code, compile_dag, mark_code_dirty) desde main.cpp para que el
// archivo principal quede acotado a la composicion de paneles ImGui.
//
// Las globals (g_source, g_descs, g_store, g_pipeline, etc.) se declaran
// extern y viven en main.cpp; aqui solo orquestamos compilacion.
namespace shaders_lab {
// Compila g_source -> programa OpenGL para g_canvas_code, refresca g_descs
// y sincroniza g_store. Actualiza g_code_err / g_code_err_line.
void compile_code();
// Compila g_pipeline -> g_dag_glsl -> programa OpenGL para g_canvas_dag.
// Actualiza g_dag_err / g_dag_err_line.
void compile_dag();
// Marca el shader Code como dirty y registra el timestamp del ultimo edit
// (para debounce de 250ms en el render loop).
void mark_code_dirty();
} // namespace shaders_lab
@@ -1,254 +0,0 @@
# Shader DAG Lab — Arquitectura y hoja de ruta
Este documento resume el diseño acumulado tras iterar en artifacts desde
"composición funcional sobre listas" hasta "DAG de shaders WebGPU con fan-in".
Sirve como contexto para retomar el proyecto en Claude Code sin perder las
decisiones de diseño.
## El problema que resuelve
Un entorno para componer fragment shaders WGSL visualmente: el usuario arrastra
"nodos" (primitivas shader) desde una paleta a un pipeline, configura
parámetros con sliders / XY pads / color pickers, y el sistema compila el DAG
resultante a un único fragment shader ejecutado en WebGPU.
Las dos vistas — grafo y código — son proyecciones del mismo modelo interno.
El usuario casual arrastra cajas; el usuario avanzado lee el WGSL generado.
## Arquitectura en una frase
```
Pipeline state (árbol JSON)
↓ compileDagToWGSL()
WGSL source
↓ device.createShaderModule()
GPU pipeline
↓ render loop, escribe uniforms cada frame
Canvas
```
Separación crítica: la **topología** del DAG (qué nodos, en qué orden, con qué
aristas) dispara recompilación del shader. Los **valores de parámetros** NO
disparan recompilación — solo se escriben al uniform buffer cada frame. Esto
hace que mover un slider sea instantáneo mientras que añadir/quitar nodos
paga el coste de compilar un nuevo pipeline.
## Modelo de datos
Un `step` del pipeline es:
```ts
type Step = {
id: string; // UUID estable (sobrevive reorders)
name: string; // clave en el catálogo NODES
params: { // valores de parámetros editables
[key: string]: number
};
meta?: { // metadatos que afectan compilación
sourceId?: string; // para blends: id del otro nodo fuente
};
};
```
El `topologyKey` que dispara recompilación se computa como:
```
pipeline.map(s => `${s.name}:${s.meta?.sourceId ?? ''}`).join('|')
```
## Catálogo de nodos
Cada entrada en `NODES` declara:
```ts
{
kind: 'gen' | 'op' | 'blend' | 'warp' | 'sdf' | 'filter' | 'modulator',
label: string,
desc: string,
params: [{ k: string, d: number }], // hasta 4 slots del vec4<f32>
controls: Control[], // descriptores de UI
body: (idx: number) => string, // emite el cuerpo WGSL
}
```
### Tipos de control UI actualmente soportados
- `slider`: rango numérico con thumb
- `xy`: pad 2D que controla dos params contiguos
- `color`: picker RGB que controla tres params contiguos
- `select`: dropdown con opciones discretas (valor = índice)
- `source`: selector del nodo fuente para blends (escribe a `meta.sourceId`)
## Compilación del DAG a WGSL
`compileDagToWGSL(pipeline)` emite un shader con esta estructura:
```wgsl
struct Uniforms {
time: f32,
_pad: f32,
resolution: vec2<f32>,
params: array<vec4<f32>, 16>,
};
@group(0) @binding(0) var<uniform> u: Uniforms;
@vertex fn vs(...) { /* fullscreen triangle */ }
// Una función por nodo, nombrada node_<idx>
fn node_0(c: vec4<f32>, uv: vec2<f32>) -> vec4<f32> { ... }
fn node_1(a: vec4<f32>, b: vec4<f32>, uv: vec2<f32>) -> vec4<f32> { ... } // blend
@fragment fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
let uv = pos.xy / u.resolution;
let out_0 = node_0(vec4<f32>(0.0), uv);
let out_1 = node_1(out_0, out_0, uv); // blend con source=out_0
return out_1;
}
```
Los outputs intermedios `out_<i>` se preservan como variables locales para
que los blends puedan referenciar nodos anteriores arbitrarios. Esto es lo
que hace que el DAG sea más que un pipeline lineal.
## Estado actual de tipos de nodo
**Implementados (lab 004):**
- `gen`: solid, gradient, plasma, checker, circle, stripes, noise (hash)
- `op`: invert, gamma, contrast, saturate, hueShift, tint, posterize, vignette, ripple, pulse
- `blend`: mix, multiply, screen, add, difference, darken, lighten, mask
**Pendientes de implementar (ver hoja de ruta abajo):**
- `warp`: distorsiones de UV (twirl, polar, kaleidoscope, pixelate, chromatic)
- `sdf`: campos de distancia con compositing (smooth_union, subtract, intersect)
- `modulator`: LFOs que producen escalares animados para alimentar parámetros
- Ruidos procedurales reales: perlin, simplex, worley, fbm
- Filtros de luminancia: threshold, levels, duotone, channel_swap
- Inputs externos: mouse como uniform adicional
## Hoja de ruta post-artifact
### Lab 005 — Warps + Ruidos + Filtros de luma + Mouse
Cambios que requiere:
- **Refactor de compilación**: generadores pasan a ser `fn sample_gen_<i>(uv) -> vec4<f32>`
para que los warps puedan modificar uv antes del muestreo.
- La cadena main mantiene dos estados: `uv` (mutable por warps) y `c` (color).
- Nuevo kind `warp` con snippets que transforman `uv`.
- Nuevo uniform `mouse: vec2<f32>` (coords 0..1) actualizado desde pointer events.
- Perlin/FBM como snippets WGSL copiados de implementaciones conocidas
(hash-based gradient noise).
### Lab 006 — Multi-pass (convoluciones + feedback)
Cambio arquitectónico mayor: cada nodo puede escribir a una textura offscreen,
el siguiente samplea esa textura. Requiere:
- Render targets intermedios (pool de texturas)
- Múltiples bind groups
- Double buffer para feedback temporal (frame N+1 lee frame N)
- Detección de qué nodos necesitan aislar su pass y cuáles pueden fusionarse
Desbloquea: blur gaussiano, sobel, edge detection, bloom, reaction-diffusion,
trails, motion blur.
### Lab 007 — SDFs tipados
Introduce heterogeneidad de tipos en las aristas del DAG:
- Aristas de tipo `field` (`f32`) vs `color` (`vec4<f32>`)
- Validación de tipos en compilación: un operador de color no acepta un field
- Nodo terminal `render_sdf` que convierte field → color con shading opciones
(planar, gradient, stroke, inflate/outline)
- Operadores SDF: `smooth_union`, `subtract`, `intersect`, `round`, `onion`
Este es el salto conceptual a "DAG tipado" que formaliza lo que el lab 001
insinuaba (el fold cambiaba de tipo la arista).
### Lab 008 — Bidireccional código ↔ grafo
Hasta ahora solo va grafo → código. El inverso requiere:
- Parser acotado del WGSL que nosotros mismos emitimos (no WGSL general)
- Marcadores en comentarios `// @meta node=<name> id=<id>` para robustez
- Detección de diff estructural para mantener posiciones / parámetros al editar
- Editor de código integrado (CodeMirror) sincronizado con el pipeline
### Lab 009 — Nodos custom definidos por usuario
Modal donde el usuario escribe body WGSL + declara params, y se registra en
la paleta como si viniera del catálogo. Cierra la asimetría código→grafo sin
necesitar parser completo. Persiste en localStorage o export/import JSON.
## Decisiones de diseño que vale la pena recordar
1. **Los IDs de nodo son UUIDs, no índices posicionales**. Las referencias
de `sourceId` sobreviven a reorderings. Los índices se re-derivan en
compilación.
2. **El patrón "armed drag"** para el drag handle: el nodo es `draggable=false`
por defecto y solo se arma a `true` cuando ocurre pointerdown sobre el
header con el handle. Esto evita que los sliders internos activen drag
accidentalmente.
3. **Uniform packing**: todos los parámetros de un nodo van en `u.params[idx]`
(un `vec4<f32>`). Si un nodo necesita más de 4 floats, habría que
reasignar slots o usar dos slots. No hay nodos hasta ahora que lo pidan.
4. **MAX_NODES = 16** es arbitrario, limitado solo por el tamaño del array
de params en el uniform buffer. Subir es trivial: cambia la constante.
5. **Las arbitrary values de Tailwind no funcionan** en algunos entornos
sin JIT. Grid templates, min-h con calc, etc. se escriben con
`style={{...}}` inline. En el proyecto de Claude Code esto no debería
ser problema si usas Tailwind 3+ con su compilador.
## Stack sugerido para Claude Code
- **Vite + React + TypeScript**: setup estándar, HMR inmediato
- **Tailwind 3+** con JIT: los arbitrary values funcionarán esta vez
- **Zustand o Jotai** para el pipeline state (se va a hacer más complejo)
- **Biome o ESLint + Prettier** para formato consistente
- Opcional pero recomendado en cuanto crezca:
- **Vitest** para tests unitarios del compilador (`compileDagToWGSL`)
- **React Testing Library** si tests de UI
- **reactflow** si en lab 008 quieres visualizar el DAG como grafo editable
## Organización de archivos sugerida
```
src/
nodes/
index.ts # export del catálogo completo
generators.ts # gen kind
operators.ts # op kind
blends.ts # blend kind
warps.ts # (lab 005)
sdfs.ts # (lab 007)
types.ts # tipos compartidos NodeDef, Control, etc.
compiler/
compileDagToWGSL.ts # la función principal
uniforms.ts # packing y escritura del buffer
validate.ts # validación de tipos (lab 007+)
webgpu/
useWebGPU.ts # el hook
renderer.ts # setup del device, context, pipeline
ui/
PipelineNode.tsx
controls/
Slider.tsx
XYPad.tsx
ColorPicker.tsx
Select.tsx
SourceSelector.tsx
Palette.tsx
Canvas.tsx
WGSLView.tsx
store/
pipeline.ts # Zustand store
App.tsx
main.tsx
```
## Primeros pasos en Claude Code
1. `npm create vite@latest shader-dag -- --template react-ts`
2. Copiar `shader-dag-blends.jsx` como base monolítica, renombrar a `.tsx`
3. Arreglar los tipos TypeScript (muchas funciones del artifact no están tipadas)
4. Romper el monolito según la estructura de arriba
5. Implementar lab 005: refactor de compilación para habilitar warps
Nota: el artifact de base (`shader-dag-blends.jsx`) funciona pero tiene el
tipado implícito de JSX. Convertir a TS te va a revelar varios tipos que
merece la pena modelar explícitamente (especialmente `Control`, que ahora es
un union discriminado informal).
@@ -1,598 +0,0 @@
import { useState, useMemo, useRef } from 'react';
import { X, Trash2, GripVertical } from 'lucide-react';
//
// Categorías morfológicas cada una con su identidad visual
//
const CATEGORIES = {
gen: {
label: 'Generadores',
subtitle: 'Anamorfismos',
signature: 'seed → [a]',
color: '#7dd3fc',
},
map: {
label: 'Transformaciones',
subtitle: 'Functor · map',
signature: '(a→b) → [a]→[b]',
color: '#c4b5fd',
},
filter: {
label: 'Filtros',
subtitle: 'Refinamientos',
signature: '[a] → [a]',
color: '#fcd34d',
},
scan: {
label: 'Scans',
subtitle: 'Folds acumulativos',
signature: '[a] → [a]',
color: '#86efac',
},
fold: {
label: 'Plegados',
subtitle: 'Catamorfismos',
signature: '[a] → b',
color: '#fda4af',
},
};
// PRNG determinista para que `random` sea reproducible
function mulberry32(seed) {
let a = seed;
return function() {
a = (a + 0x6D2B79F5) | 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
//
// Catálogo de funciones disponibles
//
const FUNCTIONS = {
// GENERADORES (anamorfismos: producen estructura desde un seed)
range: { cat: 'gen', symbol: 'range', sig: 'n → [1..n]', params: [{k:'n',d:20,min:1,max:120}], run: (_,p) => Array.from({length:p.n}, (_,i)=>i+1) },
linspace: { cat: 'gen', symbol: 'linspace', sig: 'n → [0..2π]', params: [{k:'n',d:60,min:2,max:200}], run: (_,p) => Array.from({length:p.n}, (_,i)=>(i/(p.n-1))*2*Math.PI) },
sine: { cat: 'gen', symbol: 'sine', sig: 'n → sin(4π·t)', params: [{k:'n',d:80,min:4,max:200}], run: (_,p) => Array.from({length:p.n}, (_,i)=>Math.sin((i/(p.n-1))*4*Math.PI)) },
noise: { cat: 'gen', symbol: 'noise', sig: 'seed → U(-1,1)', params: [{k:'n',d:50,min:2,max:200},{k:'seed',d:7,min:1,max:9999}], run: (_,p) => { const r=mulberry32(p.seed); return Array.from({length:p.n}, ()=>r()*2-1); } },
fib: { cat: 'gen', symbol: 'fib', sig: 'n → Fibₙ', params: [{k:'n',d:12,min:2,max:25}], run: (_,p) => { const r=[1,1]; while(r.length<p.n) r.push(r[r.length-1]+r[r.length-2]); return r.slice(0,p.n); } },
// MAPS (functor f: ab aplicado punto-a-punto)
double: { cat: 'map', symbol: '·2', sig: 'x ↦ 2x', run: (a) => a.map(x=>x*2) },
square: { cat: 'map', symbol: 'x²', sig: 'x ↦ x²', run: (a) => a.map(x=>x*x) },
negate: { cat: 'map', symbol: '-x', sig: 'x ↦ -x', run: (a) => a.map(x=>-x) },
abs: { cat: 'map', symbol: '|x|', sig: 'x ↦ |x|', run: (a) => a.map(x=>Math.abs(x)) },
sqrt: { cat: 'map', symbol: '√', sig: 'x ↦ √|x|', run: (a) => a.map(x=>Math.sqrt(Math.abs(x))) },
sin: { cat: 'map', symbol: 'sin', sig: 'x ↦ sin x', run: (a) => a.map(x=>Math.sin(x)) },
log: { cat: 'map', symbol: 'ln', sig: 'x ↦ ln(1+|x|)', run: (a) => a.map(x=>Math.log(1+Math.abs(x))) },
// FILTROS (refinamiento)
positive: { cat: 'filter', symbol: '>0', sig: 'x > 0', run: (a) => a.filter(x=>x>0) },
even: { cat: 'filter', symbol: 'par', sig: 'x even', run: (a) => a.filter(x=>Math.round(x)%2===0) },
gt: { cat: 'filter', symbol: '>t', sig: 'x > t', params: [{k:'t',d:0,min:-50,max:50,step:0.5}], run: (a,p) => a.filter(x=>x>p.t) },
// SCANS (folds acumulativos: dejan rastro)
cumsum: { cat: 'scan', symbol: 'Σ*', sig: 'Σ prefix', run: (a) => { let s=0; return a.map(x=>s+=x); } },
cummax: { cat: 'scan', symbol: 'max*', sig: 'max prefix', run: (a) => { let m=-Infinity; return a.map(x=>{m=Math.max(m,x); return m;}); } },
diff: { cat: 'scan', symbol: 'Δ', sig: 'xₙ - xₙ₋₁', run: (a) => a.map((x,i)=>i===0?0:x-a[i-1]) },
mavg: { cat: 'scan', symbol: 'μ_k', sig: 'media móvil k', params: [{k:'k',d:3,min:1,max:15}], run: (a,p) => a.map((_,i)=>{ const s=Math.max(0,i-p.k+1); const sl=a.slice(s,i+1); return sl.reduce((t,v)=>t+v,0)/sl.length; }) },
// FOLDS (catamorfismos: colapsan a escalar · terminales)
sum: { cat: 'fold', symbol: 'Σ', sig: '[a] → Σa', run: (a) => a.reduce((s,x)=>s+x, 0) },
product: { cat: 'fold', symbol: '∏', sig: '[a] → ∏a', run: (a) => a.reduce((s,x)=>s*x, 1) },
max: { cat: 'fold', symbol: 'max', sig: '[a] → max', run: (a) => a.length ? Math.max(...a) : 0 },
min: { cat: 'fold', symbol: 'min', sig: '[a] → min', run: (a) => a.length ? Math.min(...a) : 0 },
mean: { cat: 'fold', symbol: 'μ', sig: '[a] → μ', run: (a) => a.length ? a.reduce((s,x)=>s+x,0)/a.length : 0 },
count: { cat: 'fold', symbol: '#', sig: '[a] → ', run: (a) => a.length },
};
const BY_CATEGORY = Object.entries(CATEGORIES).map(([catKey, catMeta]) => ({
...catMeta,
key: catKey,
items: Object.entries(FUNCTIONS).filter(([, f]) => f.cat === catKey).map(([name, f]) => ({ name, ...f })),
}));
//
// Ejecutor: aplica el pipeline y conserva salidas intermedias
//
function executePipeline(pipeline) {
const steps = [];
let current = Array.from({length: 10}, (_, i) => i + 1); // semilla si no hay generador
let terminated = false;
for (const item of pipeline) {
const def = FUNCTIONS[item.name];
if (!def) continue;
if (terminated) { steps.push({ ...item, def, unreachable: true }); continue; }
try {
const value = def.run(current, item.params || {});
steps.push({ ...item, def, value });
if (def.cat === 'fold') terminated = true;
else current = value;
} catch (e) {
steps.push({ ...item, def, error: e.message });
terminated = true;
}
}
return { steps, terminated };
}
//
// Helpers de id y parámetros
//
let _uid = 0;
const uid = () => `n${++_uid}_${Date.now().toString(36)}`;
function defaultParams(name) {
const def = FUNCTIONS[name];
if (!def.params) return {};
return Object.fromEntries(def.params.map(p => [p.k, p.d]));
}
//
// Sparkline compacto (SVG manual)
//
function Sparkline({ data, color = '#888', w = 180, h = 34 }) {
if (!Array.isArray(data) || data.length === 0) {
return <div style={{width:w, height:h}} className="flex items-center justify-center text-[10px] text-neutral-600"></div>;
}
const min = Math.min(...data);
const max = Math.max(...data);
const span = max - min || 1;
const n = data.length;
const xStep = n > 1 ? (w - 6) / (n - 1) : 0;
const pts = data.map((v, i) => [3 + i * xStep, h - 3 - ((v - min) / span) * (h - 6)]);
const path = pts.map((p, i) => (i === 0 ? `M${p[0].toFixed(1)},${p[1].toFixed(1)}` : `L${p[0].toFixed(1)},${p[1].toFixed(1)}`)).join(' ');
const zeroY = h - 3 - ((0 - min) / span) * (h - 6);
const showZero = min < 0 && max > 0;
return (
<svg width={w} height={h} className="block">
{showZero && <line x1={0} x2={w} y1={zeroY} y2={zeroY} stroke={color} strokeOpacity={0.15} strokeDasharray="2 3" />}
<path d={path} fill="none" stroke={color} strokeWidth={1.3} strokeLinecap="round" strokeLinejoin="round" />
{n <= 40 && pts.map((p, i) => (
<circle key={i} cx={p[0]} cy={p[1]} r={1.4} fill={color} fillOpacity={0.85} />
))}
</svg>
);
}
//
// Ficha en la paleta (draggable)
//
function PaletteItem({ fnName, fn, color, onDragStart, onClick }) {
return (
<div
draggable
onDragStart={(e) => { e.dataTransfer.setData('text/fn-name', fnName); e.dataTransfer.effectAllowed = 'copy'; onDragStart?.(); }}
onClick={onClick}
className="group flex items-center gap-2 px-2.5 py-1.5 rounded-md cursor-grab active:cursor-grabbing select-none transition-colors hover:bg-neutral-800/60"
style={{ borderLeft: `2px solid ${color}` }}
title={`${fnName} :: ${fn.sig}`}
>
<span className="font-mono text-[11px] font-semibold tracking-tight" style={{ color }}>{fn.symbol}</span>
<span className="font-mono text-[11px] text-neutral-400 flex-1 truncate">{fnName}</span>
<span className="font-mono text-[9px] text-neutral-600 opacity-0 group-hover:opacity-100 transition-opacity">drag</span>
</div>
);
}
//
// Nodo del pipeline
//
function PipelineNode({ step, index, isLast, onRemove, onParamChange, onDragStart, onDragOver, onDrop, isDragging }) {
const { def, value, unreachable, error } = step;
const cat = CATEGORIES[def.cat];
const color = cat.color;
const isScalar = def.cat === 'fold';
return (
<div
draggable
onDragStart={(e) => { e.dataTransfer.setData('text/reorder-index', String(index)); e.dataTransfer.effectAllowed = 'move'; onDragStart?.(index); }}
onDragOver={(e) => { e.preventDefault(); onDragOver?.(index); }}
onDrop={(e) => onDrop?.(e, index)}
className="relative group"
style={{ opacity: isDragging ? 0.4 : 1 }}
>
<div
className="flex flex-col rounded-lg backdrop-blur-sm transition-all"
style={{
background: `linear-gradient(180deg, ${color}14 0%, ${color}06 100%)`,
border: `1px solid ${color}40`,
boxShadow: `0 0 0 1px ${color}10, 0 8px 24px -12px ${color}30`,
minWidth: 200,
}}
>
{/* cabecera */}
<div className="flex items-center gap-1.5 px-2.5 py-1.5 border-b" style={{ borderColor: `${color}25` }}>
<GripVertical size={12} className="text-neutral-600 cursor-grab" />
<span className="font-mono text-[11px] font-bold" style={{ color }}>{def.symbol}</span>
<span className="font-mono text-[10px] text-neutral-500 flex-1">{step.name}</span>
<button
onClick={() => onRemove(index)}
className="opacity-0 group-hover:opacity-100 transition-opacity text-neutral-500 hover:text-neutral-200 p-0.5 rounded"
aria-label="Eliminar"
>
<X size={11} />
</button>
</div>
{/* params */}
{def.params && (
<div className="flex items-center gap-1.5 px-2.5 py-1.5 border-b flex-wrap" style={{ borderColor: `${color}18` }}>
{def.params.map(p => (
<label key={p.k} className="flex items-center gap-1 font-mono text-[10px] text-neutral-500">
<span>{p.k}=</span>
<input
type="number"
value={step.params?.[p.k] ?? p.d}
min={p.min} max={p.max} step={p.step ?? 1}
onChange={(e) => onParamChange(index, p.k, Number(e.target.value))}
className="w-12 bg-neutral-900/60 border border-neutral-700/50 rounded px-1 py-0.5 text-neutral-200 font-mono text-[10px] focus:outline-none focus:border-neutral-500"
/>
</label>
))}
</div>
)}
{/* preview */}
<div className="px-2.5 py-1.5">
{unreachable ? (
<div className="font-mono text-[10px] text-neutral-600 italic">inalcanzable · fold anterior terminó el pipeline</div>
) : error ? (
<div className="font-mono text-[10px] text-rose-400">error: {error}</div>
) : isScalar ? (
<div className="flex items-baseline gap-2">
<span className="font-mono text-[10px] text-neutral-500">resultado</span>
<span className="font-display text-2xl font-light" style={{ color }}>{formatScalar(value)}</span>
</div>
) : (
<div>
<Sparkline data={value} color={color} w={180} h={32} />
<div className="font-mono text-[9px] text-neutral-600 mt-1">
n={value?.length ?? 0} · range [{formatScalar(Math.min(...(value?.length?value:[0])))}, {formatScalar(Math.max(...(value?.length?value:[0])))}]
</div>
</div>
)}
</div>
</div>
{/* flecha de composición */}
{!isLast && (
<div className="absolute top-1/2 -right-5 -translate-y-1/2 font-mono text-neutral-600 text-sm pointer-events-none"></div>
)}
</div>
);
}
function formatScalar(v) {
if (v === undefined || v === null || !isFinite(v)) return '—';
if (Number.isInteger(v)) return String(v);
const abs = Math.abs(v);
if (abs >= 1000 || (abs > 0 && abs < 0.01)) return v.toExponential(2);
return v.toFixed(3);
}
//
// Visualización grande del output final
//
function FinalView({ lastStep }) {
if (!lastStep) {
return (
<div className="flex-1 flex items-center justify-center text-neutral-600 font-mono text-xs">
arrastra una función para empezar
</div>
);
}
const { def, value, error, unreachable } = lastStep;
if (error || unreachable) return null;
const color = CATEGORIES[def.cat].color;
if (def.cat === 'fold') {
return (
<div className="flex flex-col items-center justify-center py-6 w-full">
<span className="font-mono text-[10px] uppercase tracking-[0.2em] text-neutral-500 mb-2">resultado · escalar</span>
<span className="font-display text-5xl font-light break-all text-center" style={{ color }}>{formatScalar(value)}</span>
</div>
);
}
// plot grande
if (!Array.isArray(value) || value.length === 0) {
return <div className="font-mono text-xs text-neutral-600 p-4"> (lista vacía)</div>;
}
const W = 720, H = 180, pad = 20;
const min = Math.min(...value), max = Math.max(...value);
const span = max - min || 1;
const n = value.length;
const xStep = n > 1 ? (W - 2 * pad) / (n - 1) : 0;
const pts = value.map((v, i) => [pad + i * xStep, H - pad - ((v - min) / span) * (H - 2 * pad)]);
const path = pts.map((p, i) => (i === 0 ? `M${p[0].toFixed(1)},${p[1].toFixed(1)}` : `L${p[0].toFixed(1)},${p[1].toFixed(1)}`)).join(' ');
const areaPath = `${path} L${pts[pts.length-1][0]},${H-pad} L${pts[0][0]},${H-pad} Z`;
const zeroY = H - pad - ((0 - min) / span) * (H - 2 * pad);
const showZero = min < 0 && max > 0;
return (
<div className="flex flex-col">
<div className="flex items-baseline justify-between mb-2">
<span className="font-mono text-[10px] uppercase tracking-[0.2em] text-neutral-500">resultado · señal</span>
<span className="font-mono text-[10px] text-neutral-500">n={n} · [{formatScalar(min)}, {formatScalar(max)}]</span>
</div>
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-auto" preserveAspectRatio="none">
<defs>
<linearGradient id="areaGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity="0.3" />
<stop offset="100%" stopColor={color} stopOpacity="0" />
</linearGradient>
</defs>
{/* grid */}
{[0.25, 0.5, 0.75].map(f => (
<line key={f} x1={pad} x2={W-pad} y1={pad + f*(H-2*pad)} y2={pad + f*(H-2*pad)} stroke="#fff" strokeOpacity={0.04} />
))}
{showZero && <line x1={pad} x2={W-pad} y1={zeroY} y2={zeroY} stroke={color} strokeOpacity={0.25} strokeDasharray="3 4" />}
<path d={areaPath} fill="url(#areaGrad)" />
<path d={path} fill="none" stroke={color} strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" />
{n <= 80 && pts.map((p, i) => (
<circle key={i} cx={p[0]} cy={p[1]} r={2} fill={color} />
))}
{/* eje */}
<line x1={pad} x2={W-pad} y1={H-pad} y2={H-pad} stroke="#fff" strokeOpacity={0.1} />
<line x1={pad} x2={pad} y1={pad} y2={H-pad} stroke="#fff" strokeOpacity={0.1} />
</svg>
</div>
);
}
//
// APP principal
//
export default function App() {
const [pipeline, setPipeline] = useState([
{ id: uid(), name: 'range', params: defaultParams('range') },
{ id: uid(), name: 'square', params: defaultParams('square') },
{ id: uid(), name: 'cumsum', params: defaultParams('cumsum') },
]);
const [dragIndex, setDragIndex] = useState(null);
const [hoverIndex, setHoverIndex] = useState(null);
const dropZoneRef = useRef(null);
const { steps } = useMemo(() => executePipeline(pipeline), [pipeline]);
const lastStep = steps[steps.length - 1];
// expresión simbólica: fold_sum map_square gen_range(20)
const expression = useMemo(() => {
if (pipeline.length === 0) return '∅';
const parts = pipeline.map(s => {
const def = FUNCTIONS[s.name];
const paramStr = def.params ? '(' + def.params.map(p => `${p.k}=${s.params?.[p.k] ?? p.d}`).join(',') + ')' : '';
return `${s.name}${paramStr}`;
}).reverse();
return parts.join(' ∘ ');
}, [pipeline]);
const addFunction = (name) => {
setPipeline(p => [...p, { id: uid(), name, params: defaultParams(name) }]);
};
const removeFunction = (index) => {
setPipeline(p => p.filter((_, i) => i !== index));
};
const changeParam = (index, key, value) => {
setPipeline(p => p.map((s, i) => i === index ? { ...s, params: { ...s.params, [key]: value } } : s));
};
const handleDropOnZone = (e) => {
e.preventDefault();
const fnName = e.dataTransfer.getData('text/fn-name');
const reorderIdx = e.dataTransfer.getData('text/reorder-index');
if (fnName && FUNCTIONS[fnName]) {
addFunction(fnName);
} else if (reorderIdx !== '') {
// soltar al final si vino de un nodo
const from = Number(reorderIdx);
setPipeline(p => {
const copy = [...p];
const [moved] = copy.splice(from, 1);
copy.push(moved);
return copy;
});
}
setDragIndex(null);
setHoverIndex(null);
};
const handleDropOnNode = (e, targetIdx) => {
e.preventDefault();
e.stopPropagation();
const fnName = e.dataTransfer.getData('text/fn-name');
const reorderIdx = e.dataTransfer.getData('text/reorder-index');
if (fnName && FUNCTIONS[fnName]) {
setPipeline(p => {
const copy = [...p];
copy.splice(targetIdx, 0, { id: uid(), name: fnName, params: defaultParams(fnName) });
return copy;
});
} else if (reorderIdx !== '') {
const from = Number(reorderIdx);
if (from === targetIdx) return;
setPipeline(p => {
const copy = [...p];
const [moved] = copy.splice(from, 1);
const adjusted = from < targetIdx ? targetIdx - 1 : targetIdx;
copy.splice(adjusted, 0, moved);
return copy;
});
}
setDragIndex(null);
setHoverIndex(null);
};
return (
<div className="w-full min-h-screen text-neutral-200" style={{
background: 'radial-gradient(ellipse at top, #0f1419 0%, #06080a 60%, #030506 100%)',
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
}}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
.font-display { font-family: 'Fraunces', serif; font-optical-sizing: auto; }
.font-mono { font-family: 'JetBrains Mono', ui-monospace, monospace; }
.grid-bg {
background-image:
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
background-size: 24px 24px;
}
/* scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.16); }
`}</style>
<div className="grid-bg min-h-screen">
{/* HEADER */}
<header className="px-6 pt-6 pb-5 border-b border-white/5">
<div className="flex items-end justify-between flex-wrap gap-4">
<div>
<div className="flex items-baseline gap-3">
<span className="font-mono text-[10px] uppercase tracking-[0.3em] text-neutral-500">lab · 001</span>
<span className="font-mono text-[10px] text-neutral-700">/</span>
<span className="font-mono text-[10px] text-neutral-500">composición funcional</span>
</div>
<h1 className="font-display text-3xl font-light tracking-tight mt-1.5">
laboratorio de <em className="italic" style={{color:'#c4b5fd'}}>morfismos</em>
</h1>
<p className="font-mono text-[11px] text-neutral-500 mt-1.5 max-w-2xl">
arrastra funciones al pipeline y componlas de izquierda a derecha. cada tipo corresponde a un esquema de recursión distinto.
</p>
</div>
<button
onClick={() => setPipeline([])}
className="flex items-center gap-2 font-mono text-[10px] uppercase tracking-wider text-neutral-500 hover:text-neutral-200 border border-white/10 hover:border-white/30 px-3 py-1.5 rounded transition-colors"
>
<Trash2 size={11} /> vaciar
</button>
</div>
</header>
{/* MAIN GRID · 2 columnas: paleta | (expresión + pipeline + visualizador apilados) */}
<main
style={{
display: 'grid',
gridTemplateColumns: '200px minmax(280px, 1fr)',
minHeight: 'calc(100vh - 140px)',
overflowX: 'auto',
}}
>
{/* ─── IZQUIERDA · PALETA ─── */}
<aside className="border-r border-white/5 p-5 overflow-y-auto" style={{maxHeight: 'calc(100vh - 140px)'}}>
<div className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500 mb-4">primitivas</div>
<div className="flex flex-col gap-5">
{BY_CATEGORY.map(cat => (
<section key={cat.key}>
<div className="flex items-center gap-2 mb-2 px-1">
<span className="w-1.5 h-1.5 rounded-full" style={{background: cat.color, boxShadow: `0 0 8px ${cat.color}`}} />
<span className="font-display text-sm italic" style={{color: cat.color}}>{cat.label}</span>
</div>
<div className="font-mono text-[9px] text-neutral-600 mb-2 px-1 leading-snug">
{cat.subtitle} · <span className="text-neutral-500">{cat.signature}</span>
</div>
<div className="flex flex-col gap-0.5">
{cat.items.map(item => (
<PaletteItem
key={item.name}
fnName={item.name}
fn={item}
color={cat.color}
onClick={() => addFunction(item.name)}
/>
))}
</div>
</section>
))}
</div>
<div className="mt-6 pt-4 border-t border-white/5 font-mono text-[9px] text-neutral-600 leading-relaxed">
tip · click o drag para añadir al pipeline. reordena arrastrando nodos existentes.
</div>
</aside>
{/* ─── DERECHA · EXPRESIÓN + PIPELINE + VISUALIZADOR ─── */}
<section className="p-4 md:p-6 overflow-y-auto flex flex-col gap-5" style={{maxHeight: 'calc(100vh - 140px)'}}>
{/* Expresión simbólica */}
<div>
<div className="flex items-baseline gap-3 mb-2">
<span className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500">expresión</span>
<span className="font-mono text-[9px] text-neutral-600">se aplica de derecha a izquierda</span>
</div>
<div className="rounded-lg bg-white/[0.03] border border-white/10 p-3">
<code className="font-mono text-xs text-neutral-200 break-all leading-relaxed">
{expression}
</code>
</div>
</div>
{/* Pipeline */}
<div>
<div className="flex items-baseline gap-3 mb-2">
<span className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500">pipeline</span>
<span className="font-mono text-[10px] text-neutral-600">{pipeline.length} {pipeline.length === 1 ? 'nodo' : 'nodos'}</span>
</div>
<div
ref={dropZoneRef}
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; }}
onDrop={handleDropOnZone}
className="rounded-xl border border-dashed border-white/10 p-5 transition-colors hover:border-white/20"
style={{background: 'rgba(255,255,255,0.015)', minHeight: '180px'}}
>
{pipeline.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center py-12 text-center">
<div className="font-display text-xl italic text-neutral-500 mb-2">zona de composición</div>
<div className="font-mono text-[10px] text-neutral-600 max-w-xs">arrastra una primitiva desde la izquierda. los nodos se encadenan de izquierda a derecha.</div>
</div>
) : (
<div className="flex flex-wrap gap-x-8 gap-y-4 items-start">
{steps.map((step, i) => (
<PipelineNode
key={step.id}
step={step}
index={i}
isLast={i === steps.length - 1}
onRemove={removeFunction}
onParamChange={changeParam}
onDragStart={setDragIndex}
onDragOver={setHoverIndex}
onDrop={handleDropOnNode}
isDragging={dragIndex === i}
/>
))}
</div>
)}
</div>
</div>
{/* Visualizador */}
<div>
<div className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500 mb-2">visualizador</div>
<div className="rounded-lg border border-white/5 bg-white/[0.015] p-4" style={{minHeight: '220px'}}>
<div className="h-full flex items-center justify-center">
<FinalView lastStep={lastStep} />
</div>
</div>
</div>
{/* Nota didáctica */}
<div className="font-mono text-[10px] text-neutral-600 leading-relaxed border-l-2 border-neutral-800 pl-3">
<span style={{color:'#7dd3fc'}}>anamorfismo</span> genera estructura · <span style={{color:'#c4b5fd'}}>functor map</span> transforma punto a punto ·{' '}
<span style={{color:'#fcd34d'}}>filtro</span> refina · <span style={{color:'#86efac'}}>scan</span> acumula dejando rastro ·{' '}
<span style={{color:'#fda4af'}}>catamorfismo</span> colapsa (es terminal)
</div>
</section>
</main>
</div>
</div>
);
}
File diff suppressed because it is too large Load Diff
@@ -1,878 +0,0 @@
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { X, AlertCircle, RotateCcw, ChevronDown, ChevronRight, Trash2, GripVertical } from 'lucide-react';
//
// CATÁLOGO DE NODOS cada uno emite un snippet WGSL
// Convención: cada nodo compila a `fn node_<idx>(c, uv) -> vec4<f32>`
// Parámetros: hasta 4 floats empaquetados en u.params[idx] (vec4<f32>)
//
const MAX_NODES = 16;
const ACCENT = '#5eead4';
const GEN_COLOR = '#5eead4';
const OP_COLOR = '#c4b5fd';
const NODES = {
// GENERADORES (ignoran c, producen nuevo color)
solid: {
kind: 'gen', label: 'solid', desc: 'color constante',
params: [
{ k: 'r', label: 'r', min: 0, max: 1, d: 0.35, step: 0.01 },
{ k: 'g', label: 'g', min: 0, max: 1, d: 0.25, step: 0.01 },
{ k: 'b', label: 'b', min: 0, max: 1, d: 0.55, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
return vec4<f32>(p.x, p.y, p.z, 1.0);`,
},
gradient: {
kind: 'gen', label: 'gradient', desc: 'gradiente en ángulo',
params: [
{ k: 'angle', label: 'ángulo', min: 0, max: 6.2832, d: 0.8, step: 0.01 },
{ k: 'hue', label: 'tono', min: 0, max: 1, d: 0.5, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
let dir = vec2<f32>(cos(p.x), sin(p.x));
let t = dot(uv - 0.5, dir) + 0.5;
let col = 0.5 + 0.5 * cos(6.28318 * (p.y + vec3<f32>(0.0, 0.33, 0.67) + t));
return vec4<f32>(col, 1.0);`,
},
plasma: {
kind: 'gen', label: 'plasma', desc: 'onda trigonométrica',
params: [
{ k: 'speed', label: 'velocidad', min: 0, max: 3, d: 1, step: 0.01 },
{ k: 'scale', label: 'escala', min: 0.5, max: 10, d: 2, step: 0.1 },
],
body: (i) => `
let p = u.params[${i}];
let t = u.time * p.x;
let col = 0.5 + 0.5 * cos(t + uv.xyx * p.y + vec3<f32>(0.0, 2.0, 4.0));
return vec4<f32>(col, 1.0);`,
},
checker: {
kind: 'gen', label: 'checker', desc: 'tablero rotando',
params: [
{ k: 'scale', label: 'escala', min: 1, max: 30, d: 8, step: 0.5 },
{ k: 'rot', label: 'rotación', min: -2, max: 2, d: 0.25, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
let aspect = u.resolution.x / u.resolution.y;
let q0 = vec2<f32>((uv.x - 0.5) * aspect, uv.y - 0.5);
let a = u.time * p.y;
let rm = mat2x2<f32>(cos(a), -sin(a), sin(a), cos(a));
let q = rm * q0 * p.x;
let chk = (floor(q.x) + floor(q.y)) - 2.0 * floor((floor(q.x) + floor(q.y)) * 0.5);
return vec4<f32>(vec3<f32>(chk), 1.0);`,
},
circle: {
kind: 'gen', label: 'circle', desc: 'sdf de círculo',
params: [
{ k: 'radius', label: 'radio', min: 0, max: 1, d: 0.4, step: 0.01 },
{ k: 'soft', label: 'suavidad', min: 0.001, max: 0.1, d: 0.008, step: 0.001 },
{ k: 'pulse', label: 'pulso', min: 0, max: 1, d: 0.1, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
let aspect = u.resolution.x / u.resolution.y;
let pos = vec2<f32>((uv.x - 0.5) * aspect, uv.y - 0.5);
let r = p.x + p.z * 0.15 * sin(u.time * 2.0);
let d = length(pos) - r;
let fill = smoothstep(p.y, -p.y, d);
return mix(c, vec4<f32>(1.0), fill);`,
},
stripes: {
kind: 'gen', label: 'stripes', desc: 'rayas animadas',
params: [
{ k: 'freq', label: 'frecuencia', min: 1, max: 80, d: 20, step: 0.5 },
{ k: 'speed', label: 'velocidad', min: -5, max: 5, d: 1, step: 0.05 },
{ k: 'angle', label: 'ángulo', min: 0, max: 3.1416, d: 0.5, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
let dir = vec2<f32>(cos(p.z), sin(p.z));
let x = dot(uv, dir);
let v = 0.5 + 0.5 * sin(x * p.x + u.time * p.y);
return vec4<f32>(vec3<f32>(v), 1.0);`,
},
noise: {
kind: 'gen', label: 'noise', desc: 'hash pseudo-aleatorio',
params: [
{ k: 'scale', label: 'escala', min: 1, max: 200, d: 80, step: 1 },
{ k: 'seed', label: 'seed', min: 0, max: 100, d: 7, step: 1 },
],
body: (i) => `
let p = u.params[${i}];
let q = floor(uv * p.x + p.y);
let h = fract(sin(dot(q, vec2<f32>(12.9898, 78.233))) * 43758.5453);
return vec4<f32>(vec3<f32>(h), 1.0);`,
},
// OPERADORES (transforman c)
invert: {
kind: 'op', label: 'invert', desc: '1 rgb',
params: [],
body: () => `
return vec4<f32>(1.0 - c.rgb, c.a);`,
},
gamma: {
kind: 'op', label: 'gamma', desc: 'pow(rgb, γ)',
params: [
{ k: 'g', label: 'γ', min: 0.1, max: 5, d: 1, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
let g = max(0.01, p.x);
return vec4<f32>(pow(max(c.rgb, vec3<f32>(0.0)), vec3<f32>(g)), c.a);`,
},
brightness: {
kind: 'op', label: 'brightness', desc: 'rgb + v',
params: [
{ k: 'v', label: 'valor', min: -1, max: 1, d: 0, step: 0.01 },
],
body: (i) => `
return vec4<f32>(c.rgb + vec3<f32>(u.params[${i}].x), c.a);`,
},
contrast: {
kind: 'op', label: 'contrast', desc: '(rgb 0.5)·k + 0.5',
params: [
{ k: 'k', label: 'k', min: 0, max: 3, d: 1, step: 0.01 },
],
body: (i) => `
return vec4<f32>((c.rgb - vec3<f32>(0.5)) * u.params[${i}].x + vec3<f32>(0.5), c.a);`,
},
saturate: {
kind: 'op', label: 'saturate', desc: 'lerp(luma, rgb, s)',
params: [
{ k: 's', label: 's', min: 0, max: 2, d: 1, step: 0.01 },
],
body: (i) => `
let luma = dot(c.rgb, vec3<f32>(0.2126, 0.7152, 0.0722));
return vec4<f32>(mix(vec3<f32>(luma), c.rgb, u.params[${i}].x), c.a);`,
},
hueShift: {
kind: 'op', label: 'hue shift', desc: 'rotar matiz',
params: [
{ k: 'h', label: 'h', min: 0, max: 1, d: 0, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
let k = vec3<f32>(0.57735);
let ca = cos(p.x * 6.2832);
let sa = sin(p.x * 6.2832);
let rot = c.rgb * ca + cross(k, c.rgb) * sa + k * dot(k, c.rgb) * (1.0 - ca);
return vec4<f32>(rot, c.a);`,
},
tint: {
kind: 'op', label: 'tint', desc: 'rgb × tinte',
params: [
{ k: 'r', label: 'r', min: 0, max: 2, d: 1, step: 0.01 },
{ k: 'g', label: 'g', min: 0, max: 2, d: 1, step: 0.01 },
{ k: 'b', label: 'b', min: 0, max: 2, d: 1, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
return vec4<f32>(c.rgb * vec3<f32>(p.x, p.y, p.z), c.a);`,
},
posterize: {
kind: 'op', label: 'posterize', desc: 'cuantizar a N niveles',
params: [
{ k: 'levels', label: 'niveles', min: 2, max: 16, d: 5, step: 1 },
],
body: (i) => `
let n = max(2.0, u.params[${i}].x);
return vec4<f32>(floor(c.rgb * n) / n, c.a);`,
},
vignette: {
kind: 'op', label: 'vignette', desc: 'oscurecer bordes',
params: [
{ k: 'strength', label: 'fuerza', min: 0, max: 2, d: 1, step: 0.01 },
{ k: 'radius', label: 'radio', min: 0, max: 1.4, d: 0.5, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
let d = length(uv - vec2<f32>(0.5));
let v = 1.0 - smoothstep(p.y, p.y + 0.3, d) * p.x;
return vec4<f32>(c.rgb * v, c.a);`,
},
ripple: {
kind: 'op', label: 'ripple', desc: 'modular brillo con ondas',
params: [
{ k: 'freq', label: 'frecuencia', min: 1, max: 100, d: 30, step: 1 },
{ k: 'amp', label: 'amplitud', min: 0, max: 1, d: 0.2, step: 0.01 },
{ k: 'speed', label: 'velocidad', min: -5, max: 5, d: 2, step: 0.05 },
],
body: (i) => `
let p = u.params[${i}];
let d = length(uv - vec2<f32>(0.5));
let w = sin(d * p.x - u.time * p.z) * p.y;
return vec4<f32>(c.rgb * (1.0 + w), c.a);`,
},
pulse: {
kind: 'op', label: 'pulse', desc: 'multiplicar por onda',
params: [
{ k: 'freq', label: 'frecuencia', min: 0, max: 10, d: 2, step: 0.05 },
{ k: 'amount', label: 'cantidad', min: 0, max: 1, d: 0.3, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
return vec4<f32>(c.rgb * (1.0 + p.y * sin(u.time * p.x)), c.a);`,
},
};
const NODES_BY_KIND = {
gen: Object.entries(NODES).filter(([, v]) => v.kind === 'gen').map(([k, v]) => ({ name: k, ...v })),
op: Object.entries(NODES).filter(([, v]) => v.kind === 'op' ).map(([k, v]) => ({ name: k, ...v })),
};
//
// Compilador: DAG WGSL
//
function compileDagToWGSL(pipeline) {
const safePipeline = pipeline.slice(0, MAX_NODES);
const fns = safePipeline.map((step, idx) => {
const def = NODES[step.name];
return `fn node_${idx}(c: vec4<f32>, uv: vec2<f32>) -> vec4<f32> {${def.body(idx)}
}`;
}).join('\n\n');
const chain = safePipeline.length === 0
? ' // pipeline vacío · fondo por defecto\n c = vec4<f32>(0.04, 0.04, 0.06, 1.0);'
: safePipeline.map((_, idx) => ` c = node_${idx}(c, uv);`).join('\n');
return `struct Uniforms {
time: f32,
_pad: f32,
resolution: vec2<f32>,
params: array<vec4<f32>, ${MAX_NODES}>,
};
@group(0) @binding(0) var<uniform> u: Uniforms;
@vertex
fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
let p = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0)
);
return vec4<f32>(p[i], 0.0, 1.0);
}
${fns}
@fragment
fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
let uv = pos.xy / u.resolution;
var c = vec4<f32>(0.0, 0.0, 0.0, 1.0);
${chain}
return c;
}
`;
}
//
// Uniform buffer: escribe los valores actuales de params + time/res
//
const UNIFORM_FLOATS = 4 + MAX_NODES * 4; // header (4) + params (MAX_NODES × 4)
const UNIFORM_BYTES = UNIFORM_FLOATS * 4; // 272 bytes
function writeUniforms(device, buffer, time, width, height, pipeline) {
const data = new Float32Array(UNIFORM_FLOATS);
data[0] = time;
data[1] = 0;
data[2] = width;
data[3] = height;
for (let i = 0; i < Math.min(pipeline.length, MAX_NODES); i++) {
const step = pipeline[i];
const def = NODES[step.name];
const offset = 4 + i * 4;
for (let j = 0; j < 4; j++) {
const p = def.params[j];
data[offset + j] = p ? (step.params[p.k] ?? p.d) : 0;
}
}
device.queue.writeBuffer(buffer, 0, data);
}
//
// Hook WebGPU + compilación de DAG
//
function useWebGPUDag(canvasRef, pipeline, topologyKey) {
const gpu = useRef({
device: null, context: null, format: null,
pipeline: null, bindGroup: null, uniformBuffer: null,
startTime: 0,
});
const pipelineRef = useRef(pipeline);
useEffect(() => { pipelineRef.current = pipeline; }, [pipeline]);
const [status, setStatus] = useState('init');
const [shaderError, setShaderError] = useState(null);
const [fps, setFps] = useState(0);
// Init GPU
useEffect(() => {
let cancelled = false;
(async () => {
try {
if (!navigator.gpu) { setStatus('unsupported'); return; }
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { setStatus('unsupported'); return; }
const device = await adapter.requestDevice();
if (cancelled) return;
const canvas = canvasRef.current;
if (!canvas) return;
canvas.width = 400; canvas.height = 400;
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format, alphaMode: 'premultiplied' });
const uniformBuffer = device.createBuffer({
size: UNIFORM_BYTES,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
gpu.current = {
...gpu.current, device, context, format, uniformBuffer,
startTime: performance.now(),
};
device.lost.then(() => setStatus('error'));
setStatus('ready');
} catch (e) {
console.error(e);
setShaderError(String(e.message || e));
setStatus('error');
}
})();
return () => { cancelled = true; };
}, [canvasRef]);
// Recompilar shader (solo en cambios de topología)
const compileShader = useCallback(async (wgsl) => {
const { device, format, uniformBuffer } = gpu.current;
if (!device) return;
device.pushErrorScope('validation');
const module = device.createShaderModule({ code: wgsl });
const info = await module.getCompilationInfo();
const errors = info.messages.filter(m => m.type === 'error');
if (errors.length > 0) {
setShaderError(errors.map(m => `línea ${m.lineNum}: ${m.message}`).join('\n'));
await device.popErrorScope();
return;
}
try {
const p = device.createRenderPipeline({
layout: 'auto',
vertex: { module, entryPoint: 'vs' },
fragment: { module, entryPoint: 'fs', targets: [{ format }] },
primitive:{ topology: 'triangle-list' },
});
const bg = device.createBindGroup({
layout: p.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: uniformBuffer } }],
});
gpu.current.pipeline = p;
gpu.current.bindGroup = bg;
setShaderError(null);
} catch (e) {
setShaderError(String(e.message || e));
}
const err = await device.popErrorScope();
if (err) setShaderError(err.message);
}, []);
useEffect(() => {
if (status !== 'ready') return;
const wgsl = compileDagToWGSL(pipelineRef.current);
compileShader(wgsl);
}, [topologyKey, status, compileShader]);
// Render loop (lee params actuales cada frame via ref)
useEffect(() => {
if (status !== 'ready') return;
let running = true;
let frames = 0;
let lastFpsSample = performance.now();
const loop = () => {
if (!running) return;
const { device, context, pipeline: gpuPipe, bindGroup, uniformBuffer, startTime } = gpu.current;
const canvas = canvasRef.current;
if (device && context && gpuPipe && bindGroup && canvas) {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));
const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w; canvas.height = h;
}
const t = (performance.now() - startTime) / 1000;
writeUniforms(device, uniformBuffer, t, canvas.width, canvas.height, pipelineRef.current);
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0, g: 0, b: 0, a: 1 },
loadOp: 'clear', storeOp: 'store',
}],
});
pass.setPipeline(gpuPipe);
pass.setBindGroup(0, bindGroup);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);
frames++;
const now = performance.now();
if (now - lastFpsSample >= 500) {
setFps(Math.round((frames * 1000) / (now - lastFpsSample)));
frames = 0; lastFpsSample = now;
}
}
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
return () => { running = false; };
}, [status, canvasRef]);
const resetTime = useCallback(() => {
gpu.current.startTime = performance.now();
}, []);
return { status, shaderError, fps, resetTime };
}
//
// Utilidades
//
let _uid = 0;
const uid = () => `n${++_uid}_${Date.now().toString(36)}`;
function defaultParams(name) {
const d = NODES[name];
return Object.fromEntries((d.params || []).map(p => [p.k, p.d]));
}
//
// Sub-componentes
//
function PaletteItem({ node, color, onDragStart, onClick }) {
return (
<div
draggable
onDragStart={(e) => {
e.dataTransfer.setData('text/node-name', node.name);
e.dataTransfer.effectAllowed = 'copy';
onDragStart?.();
}}
onClick={onClick}
className="group flex items-center gap-2 px-2 py-1 rounded cursor-grab active:cursor-grabbing select-none transition-colors hover:bg-neutral-800/60"
style={{ borderLeft: `2px solid ${color}` }}
title={node.desc}
>
<span className="font-mono text-[11px] font-medium flex-1" style={{ color }}>{node.label}</span>
<span className="font-mono text-[8px] text-neutral-600 opacity-0 group-hover:opacity-100 transition-opacity uppercase tracking-wider">drag</span>
</div>
);
}
function ParamSlider({ param, value, onChange, color }) {
const display = param.step >= 1 ? Math.round(value) : Number(value).toFixed(param.step < 0.1 ? 3 : 2);
return (
<div className="flex items-center gap-2">
<span className="font-mono text-[10px] text-neutral-400 w-14 shrink-0">{param.label}</span>
<input
type="range"
min={param.min} max={param.max} step={param.step}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
className="flex-1"
style={{ accentColor: color, minWidth: '60px' }}
/>
<span className="font-mono text-[10px] text-neutral-500 w-10 text-right tabular-nums shrink-0">{display}</span>
</div>
);
}
function PipelineNode({ step, index, onRemove, onParamChange, onDragStart, onDrop, isDragging }) {
const def = NODES[step.name];
const color = def.kind === 'gen' ? GEN_COLOR : OP_COLOR;
return (
<div
draggable
onDragStart={(e) => {
e.dataTransfer.setData('text/reorder-index', String(index));
e.dataTransfer.effectAllowed = 'move';
onDragStart?.(index);
}}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => onDrop?.(e, index)}
className="rounded-lg transition-all"
style={{
background: `linear-gradient(180deg, ${color}14 0%, ${color}06 100%)`,
border: `1px solid ${color}30`,
opacity: isDragging ? 0.4 : 1,
}}
>
<div className="flex items-center gap-2 px-2.5 py-1.5 border-b" style={{ borderColor: `${color}20` }}>
<GripVertical size={11} className="text-neutral-600 cursor-grab shrink-0" />
<span className="font-mono text-[9px] uppercase tracking-wider text-neutral-500 shrink-0">
{def.kind} · {index}
</span>
<span className="font-mono text-xs font-semibold flex-1 truncate" style={{ color }}>{def.label}</span>
<button
onClick={() => onRemove(index)}
className="text-neutral-500 hover:text-neutral-200 p-0.5"
aria-label="Eliminar"
>
<X size={11} />
</button>
</div>
{def.params.length > 0 && (
<div className="px-2.5 py-2 flex flex-col gap-1.5">
{def.params.map(p => (
<ParamSlider
key={p.k}
param={p}
value={step.params[p.k] ?? p.d}
onChange={(v) => onParamChange(index, p.k, v)}
color={color}
/>
))}
</div>
)}
{def.params.length === 0 && (
<div className="px-2.5 py-1.5 font-mono text-[9px] text-neutral-600 italic">sin parámetros</div>
)}
</div>
);
}
function StatusBadge({ status }) {
const map = {
init: { color: '#fbbf24', label: 'inicializando' },
ready: { color: ACCENT, label: 'activo' },
unsupported: { color: '#f43f5e', label: 'sin webgpu' },
error: { color: '#f43f5e', label: 'error' },
};
const s = map[status] || map.init;
return (
<span className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full" style={{ background: s.color, boxShadow: `0 0 8px ${s.color}` }} />
<span className="font-mono text-[10px]" style={{ color: s.color }}>{s.label}</span>
</span>
);
}
function StatusOverlay({ status, error }) {
if (status === 'ready') return null;
return (
<div className="absolute inset-0 flex items-center justify-center p-6 text-center" style={{ background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(4px)' }}>
{status === 'init' && <div className="font-mono text-[11px] text-neutral-400">inicializando adaptador GPU</div>}
{status === 'unsupported' && (
<div className="max-w-sm">
<div className="font-display text-xl italic mb-2" style={{ color: '#f43f5e' }}>WebGPU no disponible</div>
<div className="font-mono text-[10px] text-neutral-400 leading-relaxed">
chrome/edge 113+, safari 18+, o firefox nightly con flag. si estás en un navegador compatible, prueba abrir el artifact en pestaña nueva.
</div>
</div>
)}
{status === 'error' && (
<div className="max-w-sm">
<div className="font-display text-xl italic mb-2" style={{ color: '#f43f5e' }}>error</div>
<div className="font-mono text-[10px] text-neutral-400 whitespace-pre-wrap">{error}</div>
</div>
)}
</div>
);
}
//
// APP
//
export default function App() {
const canvasRef = useRef(null);
const [pipeline, setPipeline] = useState(() => [
{ id: uid(), name: 'plasma', params: defaultParams('plasma') },
{ id: uid(), name: 'vignette', params: defaultParams('vignette') },
]);
const [dragIndex, setDragIndex] = useState(null);
const [wgslOpen, setWgslOpen] = useState(false);
// La clave de topología cambia SOLO cuando cambia la estructura (no params)
const topologyKey = useMemo(() => pipeline.map(s => s.name).join('|'), [pipeline]);
const { status, shaderError, fps, resetTime } = useWebGPUDag(canvasRef, pipeline, topologyKey);
const generatedWGSL = useMemo(() => compileDagToWGSL(pipeline), [topologyKey]);
const addNode = (name) => {
if (pipeline.length >= MAX_NODES) return;
setPipeline(p => [...p, { id: uid(), name, params: defaultParams(name) }]);
};
const removeNode = (idx) => setPipeline(p => p.filter((_, i) => i !== idx));
const changeParam = (idx, key, value) => {
setPipeline(p => p.map((s, i) => i === idx ? { ...s, params: { ...s.params, [key]: value } } : s));
};
const handleDropOnZone = (e) => {
e.preventDefault();
const nodeName = e.dataTransfer.getData('text/node-name');
const reorderIdx = e.dataTransfer.getData('text/reorder-index');
if (nodeName && NODES[nodeName]) {
addNode(nodeName);
} else if (reorderIdx !== '') {
const from = Number(reorderIdx);
setPipeline(p => {
const copy = [...p];
const [m] = copy.splice(from, 1);
copy.push(m);
return copy;
});
}
setDragIndex(null);
};
const handleDropOnNode = (e, targetIdx) => {
e.preventDefault();
e.stopPropagation();
const nodeName = e.dataTransfer.getData('text/node-name');
const reorderIdx = e.dataTransfer.getData('text/reorder-index');
if (nodeName && NODES[nodeName]) {
setPipeline(p => {
const copy = [...p];
copy.splice(targetIdx, 0, { id: uid(), name: nodeName, params: defaultParams(nodeName) });
return copy;
});
} else if (reorderIdx !== '') {
const from = Number(reorderIdx);
if (from === targetIdx) return;
setPipeline(p => {
const copy = [...p];
const [m] = copy.splice(from, 1);
const adj = from < targetIdx ? targetIdx - 1 : targetIdx;
copy.splice(adj, 0, m);
return copy;
});
}
setDragIndex(null);
};
return (
<div className="w-full min-h-screen text-neutral-200" style={{
background: 'radial-gradient(ellipse at top, #0f1419 0%, #06080a 60%, #030506 100%)',
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
}}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
.font-display { font-family: 'Fraunces', serif; font-optical-sizing: auto; }
.font-mono { font-family: 'JetBrains Mono', ui-monospace, monospace; }
.grid-bg {
background-image:
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
background-size: 24px 24px;
}
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.16); }
input[type="range"] { height: 4px; }
`}</style>
<div className="grid-bg min-h-screen">
{/* HEADER */}
<header className="px-6 pt-6 pb-5 border-b border-white/5">
<div className="flex items-end justify-between flex-wrap gap-4">
<div>
<div className="flex items-baseline gap-3">
<span className="font-mono text-[10px] uppercase tracking-[0.3em] text-neutral-500">lab · 003</span>
<span className="font-mono text-[10px] text-neutral-700">/</span>
<span className="font-mono text-[10px] text-neutral-500">shader dag · webgpu</span>
<span className="font-mono text-[10px] text-neutral-700">/</span>
<StatusBadge status={status} />
</div>
<h1 className="font-display text-3xl font-light tracking-tight mt-1.5">
composición de <em className="italic" style={{ color: ACCENT }}>fragmentos</em>
</h1>
<p className="font-mono text-[11px] text-neutral-500 mt-1.5 max-w-2xl">
cada nodo emite un snippet WGSL · el DAG se concatena en un único fragment shader · los sliders actualizan uniforms sin recompilar
</p>
</div>
<div className="flex items-center gap-3">
<span className="font-mono text-[10px] text-neutral-600">{fps} fps</span>
<button
onClick={resetTime}
className="flex items-center gap-1.5 font-mono text-[10px] uppercase tracking-wider text-neutral-500 hover:text-neutral-200 border border-white/10 hover:border-white/30 px-3 py-1.5 rounded transition-colors"
>
<RotateCcw size={11} /> reset t
</button>
<button
onClick={() => setPipeline([])}
className="flex items-center gap-1.5 font-mono text-[10px] uppercase tracking-wider text-neutral-500 hover:text-neutral-200 border border-white/10 hover:border-white/30 px-3 py-1.5 rounded transition-colors"
>
<Trash2 size={11} /> vaciar
</button>
</div>
</div>
</header>
{/* MAIN · 3 columnas: paleta | pipeline+sliders | canvas+wgsl */}
<main style={{
display: 'grid',
gridTemplateColumns: '180px minmax(280px, 1fr) minmax(320px, 420px)',
minHeight: 'calc(100vh - 110px)',
overflowX: 'auto',
}}>
{/* ── PALETA ── */}
<aside className="border-r border-white/5 p-4 overflow-y-auto" style={{ maxHeight: 'calc(100vh - 110px)' }}>
<div className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500 mb-4">primitivas</div>
<section className="mb-5">
<div className="flex items-center gap-2 mb-2 px-1">
<span className="w-1.5 h-1.5 rounded-full" style={{ background: GEN_COLOR, boxShadow: `0 0 8px ${GEN_COLOR}` }} />
<span className="font-display text-sm italic" style={{ color: GEN_COLOR }}>generadores</span>
</div>
<div className="font-mono text-[9px] text-neutral-600 mb-2 px-1">producen color · ignoran c</div>
<div className="flex flex-col gap-0.5">
{NODES_BY_KIND.gen.map(n => (
<PaletteItem key={n.name} node={n} color={GEN_COLOR} onClick={() => addNode(n.name)} />
))}
</div>
</section>
<section>
<div className="flex items-center gap-2 mb-2 px-1">
<span className="w-1.5 h-1.5 rounded-full" style={{ background: OP_COLOR, boxShadow: `0 0 8px ${OP_COLOR}` }} />
<span className="font-display text-sm italic" style={{ color: OP_COLOR }}>operadores</span>
</div>
<div className="font-mono text-[9px] text-neutral-600 mb-2 px-1">transforman c · punto a punto</div>
<div className="flex flex-col gap-0.5">
{NODES_BY_KIND.op.map(n => (
<PaletteItem key={n.name} node={n} color={OP_COLOR} onClick={() => addNode(n.name)} />
))}
</div>
</section>
<div className="mt-6 pt-4 border-t border-white/5 font-mono text-[9px] text-neutral-600 leading-relaxed">
tip · click o drag para añadir. reordena arrastrando nodos del pipeline.
</div>
</aside>
{/* ── PIPELINE (vertical, con sliders integrados) ── */}
<section className="p-4 overflow-y-auto border-r border-white/5" style={{ maxHeight: 'calc(100vh - 110px)' }}>
<div className="flex items-baseline gap-3 mb-3">
<span className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500">pipeline</span>
<span className="font-mono text-[10px] text-neutral-600">
{pipeline.length}/{MAX_NODES} nodos
</span>
</div>
<div
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; }}
onDrop={handleDropOnZone}
className="rounded-xl border border-dashed border-white/10 p-3 transition-colors hover:border-white/20"
style={{ background: 'rgba(255,255,255,0.015)', minHeight: '300px' }}
>
{pipeline.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="font-display text-lg italic text-neutral-500 mb-1">pipeline vacío</div>
<div className="font-mono text-[10px] text-neutral-600">arrastra una primitiva de la izquierda</div>
</div>
) : (
<div className="flex flex-col gap-2">
{pipeline.map((step, i) => (
<PipelineNode
key={step.id}
step={step}
index={i}
onRemove={removeNode}
onParamChange={changeParam}
onDragStart={setDragIndex}
onDrop={handleDropOnNode}
isDragging={dragIndex === i}
/>
))}
</div>
)}
</div>
<div className="mt-4 font-mono text-[9px] text-neutral-600 leading-relaxed border-l-2 border-neutral-800 pl-3">
<span style={{ color: GEN_COLOR }}>gen</span> produce · <span style={{ color: OP_COLOR }}>op</span> transforma · el flujo es c node₀ node₁ nodeₙ
</div>
</section>
{/* ── CANVAS + WGSL ── */}
<aside className="p-4 overflow-y-auto flex flex-col gap-3" style={{ maxHeight: 'calc(100vh - 110px)' }}>
<div className="flex items-baseline gap-3">
<span className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500">output</span>
<span className="font-mono text-[9px] text-neutral-600">fragment · fullscreen triangle</span>
</div>
<div
className="rounded-xl border border-white/10 overflow-hidden relative"
style={{
aspectRatio: '1/1',
background: '#000',
boxShadow: `0 0 0 1px ${ACCENT}10, 0 20px 60px -30px ${ACCENT}30`,
}}
>
<canvas ref={canvasRef} className="block" style={{ width: '100%', height: '100%' }} />
{status !== 'ready' && <StatusOverlay status={status} error={shaderError} />}
</div>
{shaderError && status === 'ready' && (
<div className="rounded-lg p-3" style={{ background: '#f43f5e0a', border: '1px solid #f43f5e30' }}>
<div className="font-mono text-[10px] uppercase tracking-[0.2em] text-rose-400 mb-1.5 flex items-center gap-1.5">
<AlertCircle size={11} /> compilación fallida
</div>
<pre className="font-mono text-[10px] text-rose-300 whitespace-pre-wrap leading-relaxed">{shaderError}</pre>
<div className="font-mono text-[9px] text-neutral-500 mt-2">último pipeline válido sigue activo</div>
</div>
)}
{/* WGSL viewer */}
<div className="rounded-lg border border-white/5" style={{ background: 'rgba(255,255,255,0.015)' }}>
<button
onClick={() => setWgslOpen(o => !o)}
className="w-full flex items-center gap-2 px-3 py-2 font-mono text-[10px] uppercase tracking-[0.2em] text-neutral-500 hover:text-neutral-300"
>
{wgslOpen ? <ChevronDown size={11} /> : <ChevronRight size={11} />}
wgsl generado
<span className="ml-auto text-neutral-600 normal-case tracking-normal">{generatedWGSL.split('\n').length} líneas</span>
</button>
{wgslOpen && (
<pre className="font-mono text-[10px] text-neutral-400 px-3 pb-3 overflow-auto leading-relaxed" style={{ maxHeight: '260px' }}>
{generatedWGSL}
</pre>
)}
</div>
<div className="font-mono text-[9px] text-neutral-600 leading-relaxed border-l-2 border-neutral-800 pl-3">
los sliders escriben a <span style={{ color: ACCENT }}>u.params[idx]</span> sin recompilar · solo cambios de topología regeneran WGSL
</div>
</aside>
</main>
</div>
</div>
);
}
@@ -1,505 +0,0 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { Play, RotateCcw, AlertCircle } from 'lucide-react';
//
// Presets WGSL · cada uno es un shader completo autosuficiente
//
const SHADER_HEADER = `struct Uniforms {
time: f32,
resolution: vec2<f32>,
};
@group(0) @binding(0) var<uniform> u: Uniforms;
@vertex
fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
// triángulo fullscreen (truco de tres vértices)
let p = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0)
);
return vec4<f32>(p[i], 0.0, 1.0);
}
`;
const PRESETS = {
plasma: SHADER_HEADER + `
@fragment
fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
let uv = pos.xy / u.resolution;
let c = 0.5 + 0.5 * cos(u.time + uv.xyx + vec3<f32>(0.0, 2.0, 4.0));
return vec4<f32>(c, 1.0);
}
`,
circle: SHADER_HEADER + `
@fragment
fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
// SDF de un círculo con anillo y fondo sutil
let uv = (pos.xy / u.resolution) * 2.0 - 1.0;
let aspect = u.resolution.x / u.resolution.y;
let p = vec2<f32>(uv.x * aspect, uv.y);
let r = 0.4 + 0.05 * sin(u.time * 2.0);
let d = length(p) - r;
let fill = smoothstep(0.0, -0.01, d);
let ring = smoothstep(0.015, 0.0, abs(d));
let bg = vec3<f32>(0.05, 0.06, 0.08);
let col = mix(bg, vec3<f32>(0.94, 0.55, 0.72), fill) + vec3<f32>(ring * 0.9);
return vec4<f32>(col, 1.0);
}
`,
checker: SHADER_HEADER + `
@fragment
fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
// tablero rotando
let uv = pos.xy / u.resolution - 0.5;
let aspect = u.resolution.x / u.resolution.y;
let p0 = vec2<f32>(uv.x * aspect, uv.y);
let rot = u.time * 0.25;
let c = cos(rot);
let s = sin(rot);
let p = vec2<f32>(p0.x * c - p0.y * s, p0.x * s + p0.y * c) * 10.0;
let chk = (floor(p.x) + floor(p.y)) - 2.0 * floor((floor(p.x) + floor(p.y)) * 0.5);
let col = mix(vec3<f32>(0.93, 0.91, 0.86), vec3<f32>(0.10, 0.09, 0.13), chk);
return vec4<f32>(col, 1.0);
}
`,
waves: SHADER_HEADER + `
@fragment
fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
// interferencia de dos ondas senoidales
let uv = pos.xy / u.resolution;
let a = sin(uv.x * 20.0 + u.time * 2.0);
let b = sin(uv.y * 15.0 - u.time * 1.3);
let v = 0.5 + 0.5 * a * b;
let col = vec3<f32>(v, pow(v, 2.0) * 0.5 + 0.3, 1.0 - v * 0.7);
return vec4<f32>(col, 1.0);
}
`,
sdfBlob: SHADER_HEADER + `
fn sdCircle(p: vec2<f32>, r: f32) -> f32 { return length(p) - r; }
fn smoothMin(a: f32, b: f32, k: f32) -> f32 {
let h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * h * k * (1.0 / 6.0);
}
@fragment
fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
// metaball · fusión de tres SDFs
let uv = (pos.xy / u.resolution) * 2.0 - 1.0;
let aspect = u.resolution.x / u.resolution.y;
let p = vec2<f32>(uv.x * aspect, uv.y);
let t = u.time;
let c1 = sdCircle(p - vec2<f32>(cos(t) * 0.4, sin(t * 1.3) * 0.3), 0.25);
let c2 = sdCircle(p - vec2<f32>(cos(t * 0.7 + 2.0) * 0.35, sin(t * 0.9) * 0.25), 0.22);
let c3 = sdCircle(p - vec2<f32>(sin(t * 1.1) * 0.3, cos(t * 0.6) * 0.35), 0.2);
let d = smoothMin(smoothMin(c1, c2, 0.35), c3, 0.35);
let fill = smoothstep(0.02, -0.02, d);
let glow = exp(-8.0 * max(d, 0.0));
let bg = vec3<f32>(0.04, 0.02, 0.06);
let core = vec3<f32>(0.95, 0.4, 0.6);
let col = bg + core * (fill * 0.9 + glow * 0.3);
return vec4<f32>(col, 1.0);
}
`,
};
const PRESET_ORDER = [
{ key: 'plasma', label: 'plasma', hint: 'cos-gradient clásico' },
{ key: 'circle', label: 'círculo sdf', hint: 'signed distance field' },
{ key: 'checker', label: 'tablero', hint: 'patrón rotando' },
{ key: 'waves', label: 'ondas', hint: 'interferencia senoidal' },
{ key: 'sdfBlob', label: 'metaball', hint: 'fusión suave de SDFs' },
];
//
// Hook: gestión de todo el ciclo WebGPU
//
function useWebGPU(canvasRef, code) {
const gpu = useRef({
device: null,
context: null,
format: null,
pipeline: null,
bindGroup: null,
uniformBuffer: null,
startTime: 0,
raf: 0,
});
const [status, setStatus] = useState('init'); // init · ready · unsupported · error
const [shaderError, setShaderError] = useState(null);
const [fps, setFps] = useState(0);
const [timeT, setTimeT] = useState(0);
// Inicialización
useEffect(() => {
let cancelled = false;
(async () => {
try {
if (!navigator.gpu) { setStatus('unsupported'); return; }
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { setStatus('unsupported'); return; }
const device = await adapter.requestDevice();
if (cancelled) return;
const canvas = canvasRef.current;
if (!canvas) return;
canvas.width = 400;
canvas.height = 400;
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format, alphaMode: 'premultiplied' });
const uniformBuffer = device.createBuffer({
size: 16, // f32 time + pad + vec2<f32> resolution
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
gpu.current = {
...gpu.current,
device, context, format, uniformBuffer,
startTime: performance.now(),
};
device.lost.then(() => setStatus('error'));
setStatus('ready');
} catch (e) {
console.error(e);
setShaderError(String(e.message || e));
setStatus('error');
}
})();
return () => { cancelled = true; };
}, [canvasRef]);
// Compilación del shader (debounced)
const compileShader = useCallback(async (wgsl) => {
const { device, format, uniformBuffer } = gpu.current;
if (!device) return;
device.pushErrorScope('validation');
const module = device.createShaderModule({ code: wgsl });
const info = await module.getCompilationInfo();
const errors = info.messages.filter(m => m.type === 'error');
if (errors.length > 0) {
const msg = errors.map(m => `línea ${m.lineNum}: ${m.message}`).join('\n');
setShaderError(msg);
await device.popErrorScope();
return;
}
try {
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: { module, entryPoint: 'vs' },
fragment: { module, entryPoint: 'fs', targets: [{ format }] },
primitive:{ topology: 'triangle-list' },
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: uniformBuffer } }],
});
gpu.current.pipeline = pipeline;
gpu.current.bindGroup = bindGroup;
setShaderError(null);
} catch (e) {
setShaderError(String(e.message || e));
}
const err = await device.popErrorScope();
if (err) setShaderError(err.message);
}, []);
useEffect(() => {
if (status !== 'ready') return;
const id = setTimeout(() => compileShader(code), 180);
return () => clearTimeout(id);
}, [code, status, compileShader]);
// Render loop
useEffect(() => {
if (status !== 'ready') return;
let running = true;
let frames = 0;
let lastFpsSample = performance.now();
const loop = () => {
if (!running) return;
const { device, context, pipeline, bindGroup, uniformBuffer, startTime } = gpu.current;
const canvas = canvasRef.current;
if (device && context && pipeline && bindGroup && canvas) {
// redimensionado
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));
const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w;
canvas.height = h;
}
const t = (performance.now() - startTime) / 1000;
const data = new Float32Array([t, 0, canvas.width, canvas.height]);
device.queue.writeBuffer(uniformBuffer, 0, data);
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0, g: 0, b: 0, a: 1 },
loadOp: 'clear',
storeOp: 'store',
}],
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);
frames++;
const now = performance.now();
if (now - lastFpsSample >= 500) {
setFps(Math.round((frames * 1000) / (now - lastFpsSample)));
setTimeT(t);
frames = 0;
lastFpsSample = now;
}
}
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
return () => { running = false; };
}, [status, canvasRef]);
const resetTime = useCallback(() => {
gpu.current.startTime = performance.now();
}, []);
return { status, shaderError, fps, timeT, resetTime };
}
//
// APP
//
export default function App() {
const canvasRef = useRef(null);
const [code, setCode] = useState(PRESETS.plasma);
const [activePreset, setActivePreset] = useState('plasma');
const { status, shaderError, fps, timeT, resetTime } = useWebGPU(canvasRef, code);
const loadPreset = (key) => {
setActivePreset(key);
setCode(PRESETS[key]);
};
const ACCENT = '#5eead4'; // teal-300
return (
<div className="w-full min-h-screen text-neutral-200" style={{
background: 'radial-gradient(ellipse at top, #0f1419 0%, #06080a 60%, #030506 100%)',
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
}}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
.font-display { font-family: 'Fraunces', serif; font-optical-sizing: auto; }
.font-mono { font-family: 'JetBrains Mono', ui-monospace, monospace; }
.grid-bg {
background-image:
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
background-size: 24px 24px;
}
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.16); }
textarea.wgsl {
tab-size: 2;
-moz-tab-size: 2;
}
`}</style>
<div className="grid-bg min-h-screen">
{/* HEADER */}
<header className="px-6 pt-6 pb-5 border-b border-white/5">
<div className="flex items-end justify-between flex-wrap gap-4">
<div>
<div className="flex items-baseline gap-3">
<span className="font-mono text-[10px] uppercase tracking-[0.3em] text-neutral-500">lab · 002</span>
<span className="font-mono text-[10px] text-neutral-700">/</span>
<span className="font-mono text-[10px] text-neutral-500">shaders · webgpu</span>
<span className="font-mono text-[10px] text-neutral-700">/</span>
<StatusBadge status={status} />
</div>
<h1 className="font-display text-3xl font-light tracking-tight mt-1.5">
laboratorio de <em className="italic" style={{color: ACCENT}}>píxels</em>
</h1>
<p className="font-mono text-[11px] text-neutral-500 mt-1.5 max-w-2xl">
editor WGSL en vivo · cada tecla recompila el fragment shader · el uniform block expone <span style={{color: ACCENT}}>time</span> y <span style={{color: ACCENT}}>resolution</span>
</p>
</div>
<div className="flex items-center gap-3">
<span className="font-mono text-[10px] text-neutral-600">t = {timeT.toFixed(2)}s</span>
<span className="font-mono text-[10px] text-neutral-600">{fps} fps</span>
<button
onClick={resetTime}
className="flex items-center gap-1.5 font-mono text-[10px] uppercase tracking-wider text-neutral-500 hover:text-neutral-200 border border-white/10 hover:border-white/30 px-3 py-1.5 rounded transition-colors"
>
<RotateCcw size={11} /> reset t
</button>
</div>
</div>
</header>
{/* MAIN · 2 columnas: canvas | editor */}
<main style={{
display: 'grid',
gridTemplateColumns: 'minmax(280px, 1fr) minmax(340px, 1fr)',
minHeight: 'calc(100vh - 110px)',
overflowX: 'auto',
}}>
{/* IZQUIERDA · CANVAS */}
<section className="p-4 md:p-6 border-r border-white/5 flex flex-col gap-3" style={{maxHeight: 'calc(100vh - 110px)'}}>
<div className="flex items-baseline gap-3">
<span className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500">canvas</span>
<span className="font-mono text-[9px] text-neutral-600">fragment shader · fullscreen triangle</span>
</div>
<div
className="flex-1 rounded-xl border border-white/10 overflow-hidden relative"
style={{
background: '#000',
boxShadow: `0 0 0 1px ${ACCENT}10, 0 20px 60px -30px ${ACCENT}30`,
minHeight: '320px',
}}
>
<canvas
ref={canvasRef}
className="block"
style={{width: '100%', height: '100%'}}
/>
{status !== 'ready' && <StatusOverlay status={status} error={shaderError} />}
</div>
<div className="font-mono text-[9px] text-neutral-600">
consejo · el vertex shader genera un triángulo que cubre toda la pantalla. todo el trabajo interesante pasa en el <span style={{color: ACCENT}}>fragment</span>.
</div>
</section>
{/* DERECHA · EDITOR */}
<section className="p-4 md:p-6 flex flex-col gap-3" style={{maxHeight: 'calc(100vh - 110px)'}}>
{/* presets */}
<div>
<div className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500 mb-2">presets</div>
<div className="flex flex-wrap gap-1.5">
{PRESET_ORDER.map(p => {
const active = p.key === activePreset;
return (
<button
key={p.key}
onClick={() => loadPreset(p.key)}
className="font-mono text-[11px] px-2.5 py-1 rounded transition-all"
style={{
background: active ? `${ACCENT}18` : 'rgba(255,255,255,0.02)',
border: `1px solid ${active ? ACCENT + '60' : 'rgba(255,255,255,0.08)'}`,
color: active ? ACCENT : '#a3a3a3',
}}
title={p.hint}
>
{p.label}
</button>
);
})}
</div>
</div>
{/* editor */}
<div className="flex flex-col flex-1 gap-2 min-h-0">
<div className="flex items-baseline gap-3">
<span className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500">fuente · wgsl</span>
{shaderError ? (
<span className="font-mono text-[10px] text-rose-400 flex items-center gap-1">
<AlertCircle size={10} /> error de compilación
</span>
) : (
<span className="font-mono text-[10px]" style={{color: ACCENT}}> compilado</span>
)}
</div>
<textarea
className="wgsl flex-1 w-full rounded-lg p-3 font-mono text-[12px] leading-relaxed resize-none outline-none"
value={code}
onChange={(e) => setCode(e.target.value)}
spellCheck={false}
style={{
background: 'rgba(255,255,255,0.02)',
border: `1px solid ${shaderError ? '#f43f5e40' : 'rgba(255,255,255,0.08)'}`,
color: '#d4d4d4',
minHeight: '300px',
}}
/>
</div>
{/* errores */}
{shaderError && (
<div className="rounded-lg p-3" style={{
background: '#f43f5e0a',
border: '1px solid #f43f5e30',
}}>
<div className="font-mono text-[10px] uppercase tracking-[0.2em] text-rose-400 mb-1.5 flex items-center gap-1.5">
<AlertCircle size={11} /> compilación fallida
</div>
<pre className="font-mono text-[11px] text-rose-300 whitespace-pre-wrap leading-relaxed">{shaderError}</pre>
</div>
)}
<div className="font-mono text-[9px] text-neutral-600 leading-relaxed border-l-2 border-neutral-800 pl-3">
uniforms · <span style={{color: ACCENT}}>u.time</span> (f32, segundos desde inicio) · <span style={{color: ACCENT}}>u.resolution</span> (vec2&lt;f32&gt;, px). el último pipeline válido se mantiene hasta que la próxima compilación tenga éxito.
</div>
</section>
</main>
</div>
</div>
);
}
//
// Sub-componentes
//
function StatusBadge({ status }) {
const map = {
init: { color: '#fbbf24', label: 'inicializando' },
ready: { color: '#5eead4', label: 'activo' },
unsupported: { color: '#f43f5e', label: 'sin webgpu' },
error: { color: '#f43f5e', label: 'error' },
};
const s = map[status] || map.init;
return (
<span className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full" style={{background: s.color, boxShadow: `0 0 8px ${s.color}`}} />
<span className="font-mono text-[10px]" style={{color: s.color}}>{s.label}</span>
</span>
);
}
function StatusOverlay({ status, error }) {
if (status === 'ready') return null;
return (
<div className="absolute inset-0 flex items-center justify-center p-6 text-center" style={{background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(4px)'}}>
{status === 'init' && (
<div className="font-mono text-[11px] text-neutral-400">inicializando adaptador GPU</div>
)}
{status === 'unsupported' && (
<div className="max-w-sm">
<div className="font-display text-xl italic mb-2" style={{color: '#f43f5e'}}>WebGPU no disponible</div>
<div className="font-mono text-[10px] text-neutral-400 leading-relaxed">
este navegador no expone <code>navigator.gpu</code>. prueba con chrome/edge recientes, o safari 18+, o activa el flag en firefox nightly.
</div>
</div>
)}
{status === 'error' && (
<div className="max-w-sm">
<div className="font-display text-xl italic mb-2" style={{color: '#f43f5e'}}>error de inicialización</div>
<div className="font-mono text-[10px] text-neutral-400 whitespace-pre-wrap">{error}</div>
</div>
)}
</div>
);
}
-430
View File
@@ -1,430 +0,0 @@
#include "app_base.h"
#include "imgui.h"
#include "gfx/shader_canvas.h"
#include "gfx/gl_shader.h"
#include "gfx/uniform_parser.h"
#include "gfx/uniform_panel.h"
#include "gfx/dag_catalog.h"
#include "gfx/dag_compile.h"
#include "gfx/dag_uniforms.h"
#include "gfx/dag_panel.h"
#include "gfx/dag_node_editor.h"
#include "gfx/dag_palette.h"
#include "gfx/dag_node_previews.h"
#include "gfx/code_to_generator.h"
#include "gfx/shaderlab_db.h"
#include "core/panel_menu.h"
#include "core/layouts_menu.h"
#include "core/layout_storage.h"
#include "core/modal_dialog.h"
#include "core/text_input.h"
#include "core/button.h"
#include "core/tokens.h"
#include "compiler.h"
#include <chrono>
#include <cctype>
#include <cstring>
#include <string>
#include <utility>
#include <vector>
// Globals: linked extern desde compiler.cpp. NO `static` aqui.
fn::gfx::ShaderCanvas g_canvas_code;
fn::gfx::ShaderCanvas g_canvas_dag;
// Default placeholder so the Code panel does something useful on first launch
// without committing to one specific look.
static const char* CODE_PLACEHOLDER = R"glsl(// Escribe tu fragment shader aqui.
// Declara uniforms con anotaciones (// @slider, @color, @xy)
// para que aparezcan como controles al guardar como generator.
uniform vec3 u_color; // @color default=0.5,0.2,0.8
uniform float u_speed; // @slider min=0.1 max=5 default=1
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
float t = u_time * u_speed;
vec3 c = u_color * (0.5 + 0.5 * cos(t + uv.xyx + vec3(0.0, 2.0, 4.0)));
fragColor = vec4(c, 1.0);
}
)glsl";
std::string g_source = CODE_PLACEHOLDER;
std::string g_code_err;
int g_code_err_line = -1;
std::chrono::steady_clock::time_point g_code_last_edit;
bool g_code_dirty = true;
std::vector<fn::gfx::UniformDescriptor> g_descs;
fn::gfx::UniformStore g_store;
std::vector<fn::gfx::DagStep> g_pipeline;
std::string g_dag_glsl;
std::string g_dag_err;
int g_dag_err_line = -1;
static bool g_dag_dirty = true; // solo lo usa main.cpp
// ── Panel visibility (toggled from View menu and panel close button) ──────
static bool g_show_code = true;
static bool g_show_dag = true;
static bool g_show_canvas_c = true;
static bool g_show_canvas_d = true;
static bool g_show_controls = true;
static bool g_show_functions = true;
static bool g_show_generated = true;
// Tabla de paneles toggleables que fn::run_app pasa a app_menubar cada frame.
// Vive en el ambito del archivo para poder referenciarse desde main().
static constexpr fn_ui::PanelToggle k_panels[] = {
{"Code", "Ctrl+1", &g_show_code},
{"DAG Pipeline", "Ctrl+2", &g_show_dag},
{"Canvas Code", "Ctrl+3", &g_show_canvas_c},
{"Canvas DAG", "Ctrl+4", &g_show_canvas_d},
{"Controls", "Ctrl+5", &g_show_controls},
{"Functions", "Ctrl+6", &g_show_functions},
{"Generated GLSL","Ctrl+7", &g_show_generated},
};
// ── Layouts (named ImGui ini snapshots persisted in shaders_lab.db) ───────
// El storage opaco encapsula la BD y el blob pendiente. Los callbacks
// envuelven save/apply/delete/reset y se pasan a app_menubar tal cual.
static fn_ui::LayoutStorage* g_layouts = nullptr;
static fn_ui::LayoutCallbacks g_layout_cb;
// ── Save-as-generator modal state ─────────────────────────────────────────
static bool g_save_modal_open = false;
static char g_save_name[64] = "my_shader";
static char g_save_label[64] = "my shader";
static char g_save_desc[256] = "";
static char g_save_tags[128] = "shaders_lab,user";
static std::string g_save_err;
// compile_code, compile_dag, mark_code_dirty viven en compiler.cpp
using shaders_lab::compile_code;
using shaders_lab::compile_dag;
using shaders_lab::mark_code_dirty;
static void ensure_dag_default() {
if (g_pipeline.empty()) {
const fn::gfx::DagNodeDef* plasma = fn::gfx::dag_find("plasma");
if (plasma) {
fn::gfx::DagStep s;
s.id = "n_plasma";
s.name = plasma->name;
s.params = plasma->param_defaults;
g_pipeline.push_back(s);
}
}
bool has_output = false;
for (const auto& s : g_pipeline) {
const fn::gfx::DagNodeDef* d = fn::gfx::dag_find(s.name);
if (d && d->kind == fn::gfx::DagKind::Output) { has_output = true; break; }
}
if (!has_output) {
const fn::gfx::DagNodeDef* out = fn::gfx::dag_find("output");
if (out) {
fn::gfx::DagStep s;
s.id = "n_output";
s.name = out->name;
s.editor_pos_x = 500.0f;
if (!g_pipeline.empty()) s.source_ids[0] = g_pipeline.front().id;
g_pipeline.push_back(s);
}
}
}
static void draw_err(const std::string& msg, int line) {
if (msg.empty()) return;
ImGui::Separator();
if (line > 0) {
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "line %d: %s", line, msg.c_str());
} else {
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", msg.c_str());
}
}
// snake_case validation: lowercase letters, digits, underscores; first char a-z.
static bool valid_id(const char* s) {
if (!s || !*s) return false;
if (!(*s >= 'a' && *s <= 'z')) return false;
for (const char* p = s; *p; ++p) {
char c = *p;
if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_')) return false;
}
return true;
}
// Build a DagNodeDef from current Code source + form fields, persist it, and
// register in the live catalog. Returns "" on success or an error message.
static std::string save_current_as_generator() {
if (!valid_id(g_save_name)) return "name must be snake_case (a-z, 0-9, _) and start with a letter";
if (fn::gfx::dag_find(g_save_name)) {
const fn::gfx::DagNodeDef* existing = fn::gfx::dag_find(g_save_name);
if (existing && existing->is_builtin) {
return std::string("name '") + g_save_name + "' collides with a built-in node";
}
// user node with same name → overwrite is allowed
}
auto tr = fn::gfx::code_to_generator(g_source);
if (!tr.ok) return tr.err;
fn::gfx::GeneratorRecord rec;
rec.id = g_save_name;
rec.label = g_save_label[0] ? g_save_label : g_save_name;
rec.description = g_save_desc;
rec.source_glsl = g_source;
rec.body_glsl = tr.body_template;
rec.param_count = tr.param_count;
rec.param_defaults = tr.param_defaults;
rec.param_names = tr.param_names;
rec.controls = tr.controls;
rec.tags = g_save_tags;
std::string err;
if (!fn::gfx::shaderlab_db_save_generator(rec, &err)) {
return std::string("db save failed: ") + err;
}
fn::gfx::DagNodeDef def = fn::gfx::make_generator_def(rec.id, rec.label, rec.description, tr);
if (!fn::gfx::dag_register_node(def)) {
return std::string("could not register node '") + rec.id + "'";
}
return "";
}
// Reconstitute every persisted generator and inject it into the live catalog.
static void load_user_generators_into_catalog() {
for (const auto& rec : fn::gfx::shaderlab_db_list_generators()) {
// Re-translate body_template from source to keep the lambda fresh.
// (We could trust rec.body_glsl, but re-running ensures forward-compat
// when we tweak the translator.)
auto tr = fn::gfx::code_to_generator(rec.source_glsl);
if (!tr.ok) continue; // skip broken records
fn::gfx::DagNodeDef def = fn::gfx::make_generator_def(rec.id, rec.label, rec.description, tr);
fn::gfx::dag_register_node(def);
}
}
static void render() {
// Apply pending layout BEFORE any ImGui::Begin this frame.
// (LoadIniSettingsFromMemory must happen before windows are submitted.)
std::string applied = fn_ui::layout_storage_apply_pending(g_layouts);
if (!applied.empty()) g_layout_cb.active_name = applied;
if (!g_canvas_code.initialized) fn::gfx::canvas_init(g_canvas_code);
if (!g_canvas_dag.initialized) fn::gfx::canvas_init(g_canvas_dag);
if (g_code_dirty) {
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - g_code_last_edit).count();
if (elapsed > 250) {
compile_code();
g_code_dirty = false;
}
}
if (g_dag_dirty) {
compile_dag();
g_dag_dirty = false;
}
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
// Menubar (View + Layouts + Settings + About) la invoca fn::run_app a
// partir de AppConfig::panels y AppConfig::layouts_cb.
// --- Code window ---
if (g_show_code) {
if (ImGui::Begin("Code", &g_show_code)) {
if (fn_ui::button("Save as generator...", fn_ui::ButtonVariant::Secondary)) {
g_save_modal_open = true;
g_save_err.clear();
}
if (fn_ui::modal_dialog_begin("Save as generator", &g_save_modal_open,
ImVec2(420, 0))) {
ImGui::TextUnformatted("Guardar shader actual como nodo Gen del DAG.");
ImGui::Spacing();
fn_ui::text_input("name (snake_case)", g_save_name, sizeof(g_save_name),
"ej: my_shader");
fn_ui::text_input("label", g_save_label, sizeof(g_save_label));
ImGui::InputTextMultiline("description", g_save_desc, sizeof(g_save_desc),
ImVec2(380, 60));
fn_ui::text_input("tags (CSV)", g_save_tags, sizeof(g_save_tags));
if (!g_save_err.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error);
ImGui::TextWrapped("%s", g_save_err.c_str());
ImGui::PopStyleColor();
}
ImGui::Separator();
if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) {
g_save_modal_open = false;
}
ImGui::SameLine();
if (fn_ui::button("Save", fn_ui::ButtonVariant::Primary)) {
g_save_err = save_current_as_generator();
if (g_save_err.empty()) {
g_save_modal_open = false;
}
}
}
fn_ui::modal_dialog_end();
ImVec2 avail = ImGui::GetContentRegionAvail();
float footer_h = g_code_err.empty() ? 0.0f : ImGui::GetTextLineHeightWithSpacing() + 8.0f;
ImVec2 editor_size(avail.x, avail.y - footer_h);
char buf[1 << 16];
size_t copy_len = g_source.size() < sizeof(buf) - 1 ? g_source.size() : sizeof(buf) - 1;
memcpy(buf, g_source.c_str(), copy_len);
buf[copy_len] = '\0';
if (ImGui::InputTextMultiline("##code", buf, sizeof(buf), editor_size,
ImGuiInputTextFlags_AllowTabInput)) {
g_source = buf;
mark_code_dirty();
}
draw_err(g_code_err, g_code_err_line);
}
ImGui::End();
}
// --- DAG Pipeline window ---
if (g_show_dag) {
if (ImGui::Begin("DAG Pipeline", &g_show_dag)) {
if (fn::gfx::dag_node_editor(g_pipeline)) {
g_dag_dirty = true;
}
draw_err(g_dag_err, g_dag_err_line);
}
ImGui::End();
}
// --- Canvas Code ---
// Code is fully independent from the DAG: only the uniforms declared in
// the Code source itself (parsed via parse_uniforms) get fed. To reproduce
// a DAG render here, paste the *baked* "Generated GLSL" — its u_params live
// as a const array inside the source.
if (g_show_canvas_c) {
if (ImGui::Begin("Canvas Code", &g_show_canvas_c)) {
fn::gfx::canvas_render(g_canvas_code, static_cast<float>(ImGui::GetTime()),
[](unsigned int program) {
fn::gfx::uniforms_apply(g_store, g_descs, program);
});
}
ImGui::End();
}
// --- Canvas DAG ---
if (g_show_canvas_d) {
if (ImGui::Begin("Canvas DAG", &g_show_canvas_d)) {
fn::gfx::canvas_render(g_canvas_dag, static_cast<float>(ImGui::GetTime()),
[](unsigned int program) {
fn::gfx::dag_uniforms_apply(g_pipeline, program);
});
}
ImGui::End();
}
if (g_canvas_dag.program) {
fn::gfx::dag_previews_render(g_pipeline, g_canvas_dag.program);
}
// --- Controls window (Code uniforms) ---
if (g_show_controls) {
if (ImGui::Begin("Controls", &g_show_controls)) {
if (g_descs.empty()) {
ImGui::TextDisabled("No uniforms declared in Code.");
ImGui::TextDisabled("Use // @slider, @color, @toggle, @xy annotations.");
} else {
fn::gfx::uniforms_panel(g_store, g_descs);
}
// fps_overlay ahora se renderiza desde fn::run_app cuando el usuario
// lo activa en Settings → Show FPS overlay.
}
ImGui::End();
}
// --- Functions palette (drag into DAG Pipeline) ---
if (g_show_functions) {
if (ImGui::Begin("Functions", &g_show_functions)) {
fn::gfx::dag_palette();
}
ImGui::End();
}
// --- Generated GLSL window (self-contained DAG → paste-able into Code) ---
// We bake the live params into a `const vec4 u_params[]` so the displayed
// text is a complete shader: copy-pasting it into the Code editor yields
// the same render at the moment of the copy, and nothing in the DAG can
// change the Code canvas afterwards.
if (g_show_generated) {
if (ImGui::Begin("Generated GLSL", &g_show_generated)) {
if (g_pipeline.empty()) {
ImGui::TextDisabled("(DAG not compiled yet)");
} else {
static std::string s_baked;
s_baked = fn::gfx::compile_dag_to_glsl_baked(g_pipeline);
ImVec2 avail = ImGui::GetContentRegionAvail();
ImGui::InputTextMultiline("##dag_glsl",
const_cast<char*>(s_baked.c_str()),
s_baked.size() + 1,
avail,
ImGuiInputTextFlags_ReadOnly);
}
}
ImGui::End();
}
}
int main() {
fn::gfx::shaderlab_db_open("shaders_lab.db");
load_user_generators_into_catalog();
ensure_dag_default();
// Layout persistence: handle opaco que crea su propia tabla
// imgui_layouts en shaders_lab.db (CREATE IF NOT EXISTS, no toca la
// tabla ui_layouts heredada). Cualquier app del registry puede usar
// este patron.
g_layouts = fn_ui::layout_storage_open("shaders_lab.db");
fn_ui::layout_storage_make_callbacks(g_layouts, g_layout_cb);
// Override de on_reset: ademas de limpiar el INI, re-mostrar todos
// los paneles especificos de shaders_lab.
g_layout_cb.on_reset = []() {
g_show_code = g_show_dag = g_show_canvas_c = g_show_canvas_d =
g_show_controls = g_show_functions = g_show_generated = true;
ImGui::LoadIniSettingsFromMemory("", 0);
ImGui::GetIO().WantSaveIniSettings = true;
g_layout_cb.active_name.clear();
};
fn::AppConfig cfg;
cfg.title = "shaders_lab";
cfg.width = 1600;
cfg.height = 900;
cfg.about = {.name = "shaders_lab",
.version = "0.3.0",
.description = "Live GLSL shader playground with DAG pipeline. layout_storage publico, compiler extraido, AppConfig estandar, multi-viewport, modal save-as via modal_dialog."};
cfg.panels = k_panels;
cfg.panel_count = sizeof(k_panels) / sizeof(k_panels[0]);
cfg.layouts_cb = &g_layout_cb;
cfg.log = {"shaders_lab.log", 1};
cfg.auto_dockspace = false; // shaders_lab gestiona su propio DockSpace en render()
int rc = fn::run_app(cfg, render);
fn::gfx::canvas_destroy(g_canvas_code);
fn::gfx::canvas_destroy(g_canvas_dag);
fn::gfx::dag_node_editor_destroy();
fn::gfx::dag_previews_destroy();
fn_ui::layout_storage_close(g_layouts);
fn::gfx::shaderlab_db_close();
return rc;
}
-15
View File
@@ -1,15 +0,0 @@
# Smoke test app para validar que text_editor + file_watcher compilan
# y enlazan correctamente. NO es una app del registry, solo build gate
# de las funciones nuevas del issue 0025. Sin ImGui events runtime el
# test crea, settea texto, polea y destruye en 1 frame headless (no abre ventana).
add_imgui_app(text_editor_smoke
main.cpp
${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_poll_diff.cpp
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit/TextEditor.cpp
)
target_include_directories(text_editor_smoke PRIVATE
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit
)
-28
View File
@@ -1,28 +0,0 @@
---
name: text_editor_smoke
lang: cpp
domain: tools
description: "Smoke test CLI (sin GUI) que valida los wrappers PIMPL de text_editor y file_watcher (inotify Linux / ReadDirectoryChangesW Win). No abre ventana ImGui — solo crea/settea texto/lee/poll/destruye."
tags: [cpp, smoke, test, cli]
uses_functions:
- text_editor_cpp_core
- file_watcher_cpp_core
uses_types: []
framework: "cli"
entry_point: "main.cpp"
dir_path: "cpp/apps/text_editor_smoke"
repo_url: ""
---
# text_editor_smoke
Smoke test que verifica las APIs de `text_editor` y `file_watcher` linkean correctamente. Sin ventana ImGui.
## Build & run
```bash
cd cpp && cmake --build build --target text_editor_smoke -j
./build/text_editor_smoke
```
Salida esperada: log con bytes leidos del editor + eventos del file_watcher.
-64
View File
@@ -1,64 +0,0 @@
// Smoke test (no GUI): compila y ejecuta brevemente las APIs nuevas del
// issue 0025 para validar que el wrapper PIMPL del text_editor y el
// file_watcher (inotify Linux / ReadDirectoryChangesW Win) enlazan.
//
// No abre ventana ImGui — solo crea / settea texto / lee / poll / destruye.
#include "core/text_editor.h"
#include "core/file_watcher.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <thread>
#include <chrono>
int main() {
// ----- text_editor -----
auto* ed = fn::text_editor_create(fn::CodeLang::GLSL);
if (!ed) { std::fprintf(stderr, "text_editor_create returned null\n"); return 1; }
fn::text_editor_set_text(ed, "void main(){}\n");
const char* got = fn::text_editor_get_text(ed);
std::printf("text_editor: get_text -> %zu bytes\n", got ? std::strlen(got) : 0u);
if (fn::text_editor_is_dirty(ed)) {
std::fprintf(stderr, "text_editor: dirty unexpected after set_text\n");
return 1;
}
fn::text_editor_destroy(ed);
// ----- file_watcher -----
const char* path = "/tmp/fn_smoke_test.txt";
std::remove(path);
{
FILE* f = std::fopen(path, "w"); std::fputs("init\n", f); std::fclose(f);
}
auto* fw = fn::file_watcher_create();
if (!fw) { std::fprintf(stderr, "file_watcher_create returned null\n"); return 1; }
if (!fn::file_watcher_add(fw, path)) {
std::fprintf(stderr, "file_watcher_add failed: %s\n", fn::file_watcher_last_error(fw));
// Aun asi continuamos: en CI sin inotify (raro) este test seria flaky.
}
// Modificar
{
FILE* f = std::fopen(path, "w"); std::fputs("changed\n", f); std::fclose(f);
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
auto evs = fn::file_watcher_poll(fw);
std::printf("file_watcher: %zu events\n", evs.size());
for (auto& e : evs) {
const char* kind = e.kind == fn::FileEvent::Modified ? "MOD"
: e.kind == fn::FileEvent::Created ? "NEW" : "DEL";
std::printf(" [%s] %s\n", kind, e.path.c_str());
}
fn::file_watcher_destroy(fw);
std::remove(path);
std::printf("OK\n");
return 0;
}