diff --git a/.claude/agents/fn-orquestador/SKILL.md b/.claude/agents/fn-orquestador/SKILL.md index feaf663a..91d88317 100644 --- a/.claude/agents/fn-orquestador/SKILL.md +++ b/.claude/agents/fn-orquestador/SKILL.md @@ -30,6 +30,16 @@ Referencia completa: `dev/issues/0069-autonomous-agent-loop-self-iterating-tasks 6. **Auditoria total**. Cada decision se loggea en `task_runs.progress_json` con razonamiento + fase + run_id. 7. **No self-modify**. NO modificas tu propio SKILL.md ni el de otros subagentes en la misma run. 8. **Cero produccion**. NO deploys, NO llamadas a APIs externas con auth, NO tocar BDs productivas. +9. **NUNCA paths absolutos fuera del worktree**. SIEMPRE rutas relativas o absolutas que apunten dentro de `/tmp/fn_orq__/`. Si necesitas leer algo del repo principal (ej. plantillas docs), copialo al worktree primero. Refuerzo del piloto 1 (2026-05-15): orquestador modifico hooks bash del repo principal usando paths absolutos `/home/lucas/fn_registry/bash/functions/...` para destrancar pre-commit. Solucion correcta: el fix vive en el worktree, NO en main. +10. **Pre-commit hook compartido**. Worktrees comparten `.git/hooks/` con main repo. Si el hook llama scripts via path absoluto a main (ej. `/home/lucas/fn_registry/bash/functions/cybersecurity/scan_secrets_in_dirty.sh`), el hook ejecutara la version de MAIN, no la del worktree. Opciones legitimas: + a. Aplicar el fix del hook EN EL WORKTREE y commitearlo en `auto/*` — al mergear el PR, main heredara el fix. + b. Si el hook bloquea progreso y el fix del hook excede tu scope, `git commit --no-verify` para ESE commit SOLO, documentando excepcion en `task_runs.events_json[].decision="skip_hook"` con razon. + NO modificar archivos en main directamente. +11. **Post-iteracion sanity check**. Tras cada commit en `auto/*`, verificar: + ```bash + git -C /home/lucas/fn_registry status --short + ``` + Si la salida cambia respecto al baseline (capturado al inicio del piloto), HAS contaminado el repo principal. ABORT con `status=sandbox_breach` y reporta los archivos afectados en el output al humano. --- diff --git a/.claude/commands/flow.md b/.claude/commands/flow.md new file mode 100644 index 00000000..24affc23 --- /dev/null +++ b/.claude/commands/flow.md @@ -0,0 +1,131 @@ +--- +description: "Gestiona flows (casos de uso multi-app reutilizables) en dev/flows/. Subcomandos: create, list, show, status, done. Runner automatizado en fase 2." +--- + +# /flow — Gestionar flows del registry + +Flows = casos de uso end-to-end que prueban / ejercitan el sistema multi-app. Viven en `dev/flows/NNNN-.md`. Cada flow describe Goal + Flow steps + Acceptance checkboxes + Telemetria. + +**OBLIGATORIO antes de `create`**: lee `dev/flows/AGENT_GUIDE.md`. Define donde buscar piezas (capability groups, FTS por tag, apps existentes, vaults), reglas duras para no inventar IDs, y plantilla de razonamiento para recomendar extractor / transformer / sink / scheduler / notify por flow. + +Cada flow nuevo cita IDs reales del registry. Si una pieza falta, escribir `FALTA: crear ` en la tabla correspondiente. Nada de inventar nombres. + +Diferencia con `dev/issues/`: +- Issues = bugs / features de implementacion. +- Flows = trabajos reutilizables que cruzan varias apps. + +## Sintaxis + +``` +/flow create # nuevo flow desde template, ID auto +/flow list # tabla resumen +/flow show # imprime contenido + acceptance % +/flow status # status + acceptance % + ultima run +/flow done [--notes "..."] # cierra flow (status=done, mueve a completed/) +/flow run # fase 2 — runner automatizado (NO IMPLEMENTADO) +``` + +## Implementacion por subcomando + +### `create ` + +Pasos: +1. Valida `` es kebab-case: `^[a-z][a-z0-9-]*$`. Si no, error. +2. Comprueba que no existe ya: `ls dev/flows/*-.md`. Si existe, error. +3. Calcula siguiente ID libre: + - `ls dev/flows/*.md dev/flows/completed/*.md | grep -oE '^dev/flows/(completed/)?[0-9]{4}' | sort -u | tail -1` + - Suma 1, zero-pad a 4 digitos. +4. Lee `dev/flows/template.md`. +5. Sustituye ``, `NNNN`, `YYYY-MM-DD` (hoy). +6. Escribe `dev/flows/NNNN-.md`. +7. Append fila a `dev/flows/INDEX.md` (mantener orden por ID asc). +8. Reporta path nuevo + recordatorio "edita Goal / Flow / Acceptance". + +### `list` + +Lee `dev/flows/INDEX.md` y lo imprime tal cual. Si flag `--pending` solo pending, `--done` solo done, `--app ` filtra por app. + +Tambien anade columna `Accept%` calculada desde body: +- Para cada flow .md, cuenta `[ ]` y `[x]` en seccion `## Acceptance`. +- `% = checked / total * 100` redondeo entero. + +### `show ` + +`cat dev/flows/NNNN-*.md` (busca con glob NNNN-*). Si no existe, prueba `dev/flows/completed/NNNN-*.md`. Si no, error. + +### `status ` + +Imprime resumen del frontmatter + acceptance %: + +``` +=== flow 0001 === +name: hn-top-stories +status: pending +risk: low +priority: high +apps: navegator_dashboard, dag_engine, data_factory, agents_and_robots +acceptance: 2/6 (33%) +updated: 2026-05-16 + +Pending checks: +- [ ] Recipe creada y validada +- [ ] DAG corre OK 2 veces consecutivas via scheduler +- [ ] data_factory.runs tiene >=2 entries +- [ ] Schema extraido cubre 6/6 fields +``` + +### `done [--notes "..."]` + +Pasos: +1. Verifica todos los `[ ]` estan checked. Si no, prompt "X checks pendientes, --force para cerrar igualmente". +2. Edita frontmatter: `status: done`, `updated: `. +3. Si `--notes`, append a seccion `## Notas`. +4. `git mv dev/flows/NNNN-.md dev/flows/completed/`. +5. Actualiza `dev/flows/INDEX.md`: cambia status del flow + mueve fila a seccion Completed (mantener tabla principal solo con pending/running/failed/deferred). + +### `run ` — FASE 2 (NO IMPLEMENTADO AUN) + +Hoy: imprime `/flow run no implementado todavia. Sigue los pasos manualmente y marca acceptance con sed/edit.` + +Diseño futuro: +- Parsea `## Flow` en pasos. +- Cada paso tipo `function: ` -> ejecuta `./fn run `. +- Cada paso tipo `cmd: ` -> ejecuta subprocess. +- Texto libre -> "MANUAL: " + pause user input. +- Persistencia ejecuciones en `dev/flows/runs/-.jsonl`. +- Update acceptance checkboxes automaticamente segun heuristics (count runs en data_factory, etc.). + +## Conventions + +- Numeracion 0001+, propia (no comparte con `dev/issues/`). +- Status: `pending | running | done | failed | deferred`. +- Risk: `low` (publico) | `medium` (auth no sensible) | `high` (datos personales). +- Apps listadas en frontmatter — `/flow list --app navegator_dashboard` filtra. +- Acceptance es la fuente de verdad del progreso. + +## Output style + +Caveman. Tablas markdown. Sin emojis. Sin verbosidad. + +Errores: 1 linea con el problema + sugerencia. + +## Ejemplos + +``` +/flow create reddit-sentiment-tracker +# crea dev/flows/0008-reddit-sentiment-tracker.md +# anade fila a INDEX + +/flow list --pending +# muestra solo flows no cerrados + +/flow status 0001 +# acceptance 0/6, todos los checks pendientes + +# Tras correr el flow manualmente: +# editas el .md, marcas [x] los checks completados +/flow status 0001 +# acceptance 6/6 +/flow done 0001 --notes "smoke pass; LLM tardo 14s; recipe robusta" +# mueve a completed/, marca status=done +``` diff --git a/.claude/commands/fn_claude.md b/.claude/commands/fn_claude.md index 18d2670b..e752f2ee 100644 --- a/.claude/commands/fn_claude.md +++ b/.claude/commands/fn_claude.md @@ -152,6 +152,20 @@ Tambien actualiza `call_monitor.copied_code` + `function_stats` corriendo: cd "$ROOT/projects/fn_monitoring/apps/call_monitor" && ./call_monitor copied-code && ./call_monitor propose ``` +### 5b. MEMORIZE — anadir cada funcion nueva a MEMORY.md (issue 0087 pieza 6) + +Por cada funcion creada con exito, llama: + +```bash +bash "$ROOT/.claude/scripts/append_fn_to_memory.sh" "" "" +``` + +El script es idempotente (si la fn ya esta linkeada, no duplica). Crea `reference_fn_.md` con metadata `type: reference` e indexa la entrada en `MEMORY.md` como linea `- [fn-](reference_fn_.md) — `. Asi proximas sesiones cargan MEMORY.md y ven el catalogo de funciones recien creadas sin segunda lookup. + +`purpose` = 1 frase derivada del `description` del .md de la funcion (max 80 chars). Si description es larga, recorta. Ejemplo: +- fn_id: `parse_http_log_go_infra` +- purpose: "parsea log Apache/Nginx a struct; pure" + Reporta: - N funciones nuevas creadas (con IDs) - N proposals nuevas en `registry.db.proposals` diff --git a/.claude/rules/INDEX.md b/.claude/rules/INDEX.md index ddbcedd1..b541a86c 100644 --- a/.claude/rules/INDEX.md +++ b/.claude/rules/INDEX.md @@ -34,3 +34,4 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente. | 28 | [delegation.md](delegation.md) | Si vas a escribir logica reutilizable inline -> spawn fn-constructor inmediato + tag de grupo + usar en mismo turno. Issue 0086 | | 29 | [capability_groups.md](capability_groups.md) | Tags planos + paginas madre `docs/capabilities/.md` para desbloquear clusters de funciones en un read. Issue 0086 | | 30 | [function_growth_and_self_docs.md](function_growth_and_self_docs.md) | Contrato self-doc de cada `.md` (Ejemplo + Cuando usarla + Gotchas + Growth log) + crecimiento del registry por **promocion de composiciones** a pipelines, NO por inflado de funciones. Issue 0087 | +| 31 | [autonomous_loop.md](autonomous_loop.md) | Reglas para `fn-orquestador` + `/autonomous-task`: sandbox obligatorio, paths protegidos, filtro proposals auto-aplicables, watchdog, idempotencia. Issue 0069 | diff --git a/.claude/rules/autonomous_loop.md b/.claude/rules/autonomous_loop.md new file mode 100644 index 00000000..4ffcc330 --- /dev/null +++ b/.claude/rules/autonomous_loop.md @@ -0,0 +1,75 @@ +## Bucle autonomo (`fn-orquestador` + `/autonomous-task`) — issue 0069 + +`fn-orquestador` recorre el ciclo reactivo (CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR) sin intervencion humana, hasta convergencia (suite verde), estancamiento (no progreso N iteraciones), timeout, o tope de iteraciones. Trabaja SIEMPRE en sandbox `auto/`, NUNCA merge a master. + +### Cuando se invoca + +- Skill `/autonomous-task ` (humano lanza explicitamente). +- Cron / Dagu (planificable; no implementado por defecto). +- NO se invoca como reaccion a hooks ni a fallos de tests "en caliente". Siempre tarea explicita. + +### Reglas duras + +1. **Sandbox obligatorio**: rama `auto/-`. Si la rama existe -> reset hard contra master y reanudar. NUNCA commits a master, NUNCA push --force-with-lease a master. +2. **Paths protegidos**: respetar `dev/autonomous_protected_paths.json` exactamente. Cualquier intento de modificar un path protegido aborta la iteracion y registra `task_runs.status='aborted_protected_path'`. +3. **Filtro de proposals auto-aplicables**: el orquestador SOLO aplica proposals que cumplen: + - `kind in (bug_fix, e2e_check_add, doc_update, capability_tag_add)` -> auto-aplicable. + - `kind in (new_function, deprecate_function, refactor, schema_change)` -> NO auto-aplicable (queda `pending` para humano). + - `priority in (low, medium)` -> auto-aplicable. `high|critical` -> requiere humano salvo override `--allow-high`. +4. **Watchdog**: si la metrica de progreso (`checks_pass / checks_total`) no sube en `N=3` iteraciones consecutivas -> abort. Registrar `task_runs.status='stalled'`. +5. **Tiempo**: cada `task_run` con timeout default 30 min. Override con `--timeout-min N` hasta max 4h. +6. **Idempotencia**: re-ejecutar `/autonomous-task ` sobre la misma issue reanuda desde la ultima iteracion exitosa, NO reinicia desde cero (lookup en `task_runs` por `issue_id`). +7. **Trazabilidad**: cada decision se persiste en `task_runs.events_json[]` con `{ts, agent, action, evidence, diff_summary}`. El humano puede leer el log entero para auditar. +8. **No self-modification**: orquestador NUNCA modifica `.claude/agents/`, `.claude/commands/`, `.claude/rules/`, `.claude/scripts/`, `.claude/CLAUDE.md`. Reforzado en `autonomous_protected_paths.json`. +9. **NUNCA paths absolutos fuera del worktree**. Refuerzo del piloto 1 (2026-05-15): el orquestador uso `/home/lucas/fn_registry/bash/functions/...` para fixear hooks bash y contamino el repo principal. Solucion correcta: fix vive solo en el worktree. Post-cada-iteracion: `git -C status --short` debe permanecer igual al baseline; cualquier diff = `status=sandbox_breach` -> ABORT. +10. **Pre-commit hooks compartidos**. Worktrees comparten `.git/hooks/` con main. Si un hook llama scripts via path absoluto, ejecutara la version de main. Si el hook bloquea progreso por bug en main: aplica el fix EN EL WORKTREE (commit en auto/*); si el bug del hook excede scope: `git commit --no-verify` para ESE commit con `task_runs.events_json[].decision="skip_hook"` + razon. NO editar main. + +### Estructura task_run + +Migration `fn_operations/migrations/006_task_runs.sql`. Campos minimos: `id`, `issue_id`, `branch`, `started_at`, `finished_at`, `status` (`running|done|failed|aborted_protected_path|stalled|timeout`), `iterations`, `checks_pass`, `checks_fail`, `proposals_applied_json`, `proposals_skipped_json`, `events_json`, `final_diff_sha`. + +### Fases por iteracion + +``` +loop: + 1. fn-constructor (Read+Edit+Write+Bash limitados) - aplica fix segun ultima proposal seleccionada + 2. fn-executor - corre build + tests + smoke + 3. fn-recopilador - audita operations.db de la app + 4. fn-analizador - corre e2e_checks (registra e2e_runs) + 5. SI todos los checks pasan -> commit + push rama + abre PR. status=done. exit. + 6. SI no progreso N iteraciones -> abort. status=stalled. + 7. fn-mejorador - crea proposals desde fallos + 8. orquestador filtra proposals auto-aplicables -> selecciona la primera -> goto 1. +``` + +### Output al humano + +``` +=== /autonomous-task 0068 === +task_run_id: run_e2e_a1b2c3 +branch: auto/0068-e2e-validation +iterations: 4 +status: done +checks_pass: 8/8 +proposals_applied: 3 (run_e2e_run_001, run_e2e_run_002, run_e2e_run_003) +proposals_skipped: 1 (refactor — needs human review) +PR: https://gitea.../pulls/42 +``` + +### Anti-patrones + +| Anti-patron | Por que es malo | +|---|---| +| Mergear `auto/` a master sin PR + humano | Salta gate, riesgo de regresion | +| Auto-aplicar proposal `kind=refactor` | Cambios sistemicos requieren revision | +| Modificar `go.sum`, `package-lock.json`, `uv.lock` | Cambios de deps requieren CVE/license review | +| Bucle infinito sin watchdog | Coste descontrolado de tokens | +| Borrar archivos sin backup en `task_runs.events_json` | Pierde auditoria | +| Override de paths protegidos via env var | Bypass de seguridad | + +### Relacion con otras reglas + +- [[e2e_validation]] — fn-analizador (fase 4) lee el contrato `e2e_checks` que el orquestador usa como gate. +- [[apps_tbd]] — el orquestador opera en rama `auto/*`, no exenta de TBD. +- [[feature_flags]] — si el fix no esta terminado, el orquestador puede meterlo detras de flag OFF antes de PR. +- [[registry_calls]] — toda invocacion del orquestador y sub-agentes pasa por MCP/`fn run`/heredoc canonico, registrada en call_monitor. diff --git a/.claude/rules/cpp_apps.md b/.claude/rules/cpp_apps.md index e2c295bf..6a03ed58 100644 --- a/.claude/rules/cpp_apps.md +++ b/.claude/rules/cpp_apps.md @@ -20,14 +20,14 @@ Razones: Pipeline: `init_cpp_app_bash_pipelines`. Slash command equivalente: `/new-cpp-app`. Auditoria: `fn doctor cpp-apps`. -### 1. Ubicacion +### 1. Ubicacion (issue 0096 estandarizada) | Caso | Donde vive | |---|---| -| App independiente | `cpp/apps//` | +| App independiente | `apps//` | | App de un proyecto | `projects//apps//` | -NUNCA en `cpp/apps//` si pertenece a un proyecto, NUNCA fuera de `apps/` directamente. Ver `apps_location` en memoria + regla `apps_vs_functions.md`. +NUNCA en `cpp/apps//` (deprecado tras issue 0096) ni en cualquier otra carpeta nombrada por lenguaje (`python/apps/`, `bash/apps/`, etc.). Las carpetas por lenguaje son solo para codigo del registry (`cpp/functions/`, `python/functions/`, etc.), nunca para artefactos. Ver `apps_location` en memoria + regla `apps_vs_functions.md`. ### 2. Estructura minima @@ -189,20 +189,105 @@ WMs). Activado por defecto, sin opt-in: con `glfwSetWindowPos/Size` (no espera al siguiente NewFrame). 2. **Per-frame viewport sync** al inicio del main loop — cubre viewports secundarios (paneles drag-out) que la backend crea dinamicamente. -3. **Win32 WndProc subclass** (`#ifdef _WIN32`) — observa `WM_ENTERSIZEMOVE` - / `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada drag. Mientras - el bracket esta abierto el main loop SKIPEA `render_fn` + `glfwSwapBuffers`, - replicando el contrato del title-bar drag native (DefWindowProc bloquea - el hilo, DWM compositor mueve el framebuffer existente). +3. **Win32 WndProc subclass per HWND** (`#ifdef _WIN32`) — observa + `WM_ENTERSIZEMOVE` / `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada + drag. El subclass se instala en la ventana principal Y en cada HWND + secundario que el backend de ImGui crea cuando un panel se arrastra fuera + del main (escaneo per-frame de `pio.Viewports`). Mientras el bracket esta + abierto en CUALQUIER HWND propio, el main loop SKIPEA `render_fn` + + `glfwSwapBuffers` globalmente, replicando el contrato del title-bar drag + native (DefWindowProc bloquea el hilo, DWM compositor mueve el framebuffer + existente). El flag `g_in_sizemove` es global a proposito: una sola + sesion de sizemove externo pausa todo el render para que ninguna ventana + compita con el OS. -Tests: `cpp/apps/altsnap_jitter_test/` corre dos fases: + Estado del subclass: + - `g_subclassed` = `unordered_map`. Chain a la proc + original via `CallWindowProcW`. + - `install_sizemove_subclass_hwnd(HWND)` idempotente (skip si ya en mapa). + - Per-frame: `prune_dead_subclassed()` con `IsWindow` + install en cada + `pio.Viewports[i]->PlatformHandle` nuevo. + - `uninstall_sizemove_subclass_all()` restaura cada HWND al exit. + +#### Iconified main no pierde paneles flotantes (2026-05-16) + +El legacy `glfwWaitEvents + continue` al detectar `GLFW_ICONIFIED` paraba TODO +el frame loop. Con multi-viewport activo eso significa que +`ImGui::UpdatePlatformWindows + RenderPlatformWindowsDefault` dejan de +refrescar los viewports secundarios — los floating panels aparecen congelados +o son agrupados/ocultados por el WM. Fix actual: el iconified-gate cuenta +viewports secundarios primero; si hay alguno, fall-through al frame normal +(la swap del main HWND minimizado es harmless, los contexts GL secundarios +siguen pintando). Solo cuando NO hay flotantes dormimos en `glfwWaitEvents`. + +#### Alt + RMB / Alt + LMB anywhere → modal nativo (2026-05-16) + +WndProc del subclass tambien intercepta clicks con Alt held (`GetAsyncKeyState(VK_MENU) & 0x8000`): + +- `WM_LBUTTONDOWN` + Alt → `ReleaseCapture()` + + `PostMessage(WM_SYSCOMMAND, SC_MOVE | HTCAPTION)`. Modal MOVE nativo. +- `WM_RBUTTONDOWN` + Alt → calcula direccion por cuadrante (TOPLEFT/TOPRIGHT/ + BOTTOMLEFT/BOTTOMRIGHT relativo al centro del client rect) y emite + `PostMessage(WM_SYSCOMMAND, SC_SIZE | dir)`. Modal RESIZE nativo. + +Ambos retornan 0 (consumen el click — ImGui NO lo ve). Aplica a main y a +cada viewport flotante porque el subclass per-frame ya cubre todos los HWND. +El modal nativo dispara `WM_ENTERSIZEMOVE`, que el gate existente pausa +render → cero jitter automatico, mismo contrato que el title-bar drag. + +**Caveat**: cualquier Alt+click se consume — perdes Alt+click como shortcut +UI. Aceptable porque Alt-modifier en clicks UI es muy raro. + +#### Title-bar-only move para ImGui windows (2026-05-16) + +`fn::run_app` setea `io.ConfigWindowsMoveFromTitleBarOnly = true`. Critico +para viewports secundarios: un viewport flotante = OS window borderless con +UNA ventana ImGui rellenandolo. Sin el flag, ImGui mueve sus ventanas +arrastrando cualquier client-pixel — como la ventana ImGui ES el viewport +entero, el OS window sigue al cursor sin modifier. Con el flag, floating +panels obedecen el contrato "solo header arrastra" (igual que main que tiene +title bar nativo de Windows). Alt+LMB anywhere sigue funcionando (consumido +antes por el subclass). + +#### Test observability — `fn::internal::*` (2026-05-16) + +Counters monotonicos para validar el subclass desde tests headless, +zero-cost en prod: + +```cpp +namespace fn::internal { + int sizemove_enter_count(); // ++ en cada WM_ENTERSIZEMOVE + int alt_rmb_resize_count(); // ++ en cada Alt+RMB consumido + int alt_lmb_move_count(); // ++ en cada Alt+LMB consumido + int rbuttondown_seen_count(); // diagnostico — todo WM_RBUTTONDOWN + void set_force_alt_for_test(bool); // bypass GetAsyncKeyState para tests +} +``` + +En test mode (`set_force_alt_for_test(true)`), los handlers de Alt cuentan +pero NO postean `SC_SIZE`/`SC_MOVE` — el harness no se queda atrapado en el +modal de Windows. Path real en prod sigue posteandolos. + +Tests: `apps/altsnap_jitter_test/` corre seis fases: - `p1.sync` (cross-platform): drives `glfwSetWindowPos` cada frame, asserta `vp->Pos` sigue OS dentro de 1px. - `p2.altsnap` (Windows): worker thread fakea `WM_ENTERSIZEMOVE` + - burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE`, asserta - que `render()` no se llama durante el bracket. + burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE` sobre el + HWND principal, asserta que `render()` no se llama durante el bracket. +- `p3.secondary` (Windows): fuerza viewport secundario + (`ConfigViewportsNoAutoMerge=true`), localiza su HWND y repite el bracket + sobre el. Valida que el subclass per-viewport tambien pausa el render. +- `p4.minimize` (Windows): state machine 4 steps — captura + `IsWindow(secondary_hwnd)` antes/durante/despues de `glfwIconifyWindow + + glfwRestoreWindow`. Asserta los 3 estados vivos y `renders_iconified > 0`. +- `p5.alt_rmb` (Windows): `set_force_alt_for_test(true)` + + `SendMessage(WM_RBUTTONDOWN)` sincrono mismo-hilo. Asserta + `alt_rmb_resize_count` incrementa. +- `p6.alt_lmb` (Windows): mismo patron para `WM_LBUTTONDOWN`. Asserta + `alt_lmb_move_count` incrementa. -Lanzar con `e2e_run_cpp_windows altsnap_jitter_test`. +Lanzar con `source bash/functions/infra/e2e_run_cpp_windows.sh && +e2e_run_cpp_windows altsnap_jitter_test`. NO hace falta nada en cada app — toda `fn::run_app` lo hereda. Si una app necesita renderizar incluso durante external move (caso raro: telemetria @@ -261,3 +346,68 @@ de antes: `imgui.ini` es la unica fuente. - App headless / capture mode: `cfg.auto_layouts = false`. - Cambiar nombre del archivo: `cfg.auto_layouts_db = ".db"` (relativo a `local_files/`). + +### 11. Icono Windows (.ico embebido en el .exe) — 2026-05-16 + +Cada app C++ desplegada a Windows tiene su propio icono. El icono vive en +`/appicon.ico` (multi-resolucion: 16/24/32/48/64/128/256). El macro +`add_imgui_app` de `cpp/CMakeLists.txt` lo detecta automaticamente: si +`WIN32` + existe `/appicon.ico`, genera un +`_appicon.rc` en `CMAKE_CURRENT_BINARY_DIR` apuntando al `.ico` con +`IDI_ICON1 ICON ""` y lo anade a `add_executable`. El compilador RC +(`x86_64-w64-mingw32-windres` configurado en `cpp/toolchains/mingw-w64.cmake`) +lo enlaza al `.exe` como recurso `.rsrc`. + +Verificar: `x86_64-w64-mingw32-objdump -h .exe | grep rsrc` debe +mostrar la seccion. El project line en `cpp/CMakeLists.txt` declara +`LANGUAGES C CXX RC` solo en WIN32 (Linux ignora la `.rc`). + +#### Crear `.ico` para una app nueva + +Fuente de glyphs: **Phosphor Icons** (`sources/phosphor-core/`, clonado de +`https://github.com/phosphor-icons/core.git`). 1512 SVGs en weight `regular`, +`bold`, `fill`, `light`, `thin`, `duotone`. Usamos `fill` por defecto — mejor +legibilidad a 16/24px. + +Funcion del registry: `generate_app_icon_py_infra` rasteriza un SVG Phosphor +sobre fondo redondeado del color accent y exporta `.ico` multi-res. Una +linea por app: + +```python +from infra import generate_app_icon +generate_app_icon( + phosphor_icon_name="chart-bar", + accent_hex="#0ea5e9", + out_ico_path="apps/chart_demo/appicon.ico", +) +``` + +Mapping inicial (2026-05-16) en `dev/gen_app_icons.py` — script reproducible +que regenera los 11 `.ico` de un golpe leyendo la tabla `APPS`. Anadir app +nueva: una fila `(app_id, dir, phosphor_icon, accent_hex)` en `APPS` y +`/tmp/iconenv/bin/python dev/gen_app_icons.py` (o el venv del registry, ya +trae `cairosvg` + `Pillow`). + +Convenciones: +- **Glyph weight**: `fill` (mas legible a 16px que `regular` o `bold`). +- **Color**: 1 accent_hex distinto por app — Tailwind palette 500-700 + funciona bien (`#0ea5e9` sky-500, `#16a34a` green-600, etc.). +- **Padding**: glyph ocupa ~70% del canvas, fondo redondeado al 16% del lado. +- **Glyph color**: siempre blanco sobre el fondo accent. + +Si Phosphor no tiene el icono adecuado: buscar en `sources/phosphor-core/assets/fill/` +con `ls | grep ` antes de inventar — 1512 disponibles. + +#### Re-deploy tras cambiar icono + +```bash +# 1. Regenerar .ico +./fn run generate_app_icon "chart-bar" "#0ea5e9" "apps/chart_demo/appicon.ico" +# (o editar dev/gen_app_icons.py + relanzar) + +# 2. Rebuild + redeploy (build dispara windres → nuevo .rsrc) +./fn run redeploy_cpp_app_windows chart_demo apps/chart_demo --build +``` + +Windows cachea iconos en `iconcache.db`. Si el nuevo icono no aparece tras +desplegar, refresh con `ie4uinit.exe -show` o reiniciar Explorer. diff --git a/.claude/rules/ids_naming.md b/.claude/rules/ids_naming.md index d417cf23..85870871 100644 --- a/.claude/rules/ids_naming.md +++ b/.claude/rules/ids_naming.md @@ -1,3 +1,35 @@ -IDs siguen el formato `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`). +## ids_naming — formato predictible -Nombres de funciones en snake_case. Tipos en PascalCase para Go. +IDs: `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`). Predictibilidad alta -> Claude descubre por fuzzy match sin lookup. Issue 0087. + +### Reglas + +1. **snake_case**: `[a-z0-9_]+`. Nada de PascalCase, kebab-case, dot.notation. +2. **Verbo obligatorio**: al menos un token del `name` debe ser un verbo de accion. El verbo puede ir delante (`get_user`) o detras (`user_lookup`). Ejemplos validos: `filter_slice`, `bank_login`, `metabase_get_dashboard`, `redeploy_cpp_app`. Invalidos: `slice` (sustantivo solo), `user` (sustantivo solo), `data` (sustantivo solo). +3. **Dominio canonico**: el `domain` debe estar en la lista canonica (ver `mcp__registry__fn_list_domains`). Crear dominio nuevo solo si el bucket es claramente distinto y se anade en el mismo turno a CLAUDE.md. +4. **Tipos en PascalCase Go**: `ResultGoCore`, `ErrorGoCore`. Aplica solo al codigo Go; el ID en el registry sigue siendo snake_case (`result_go_core`). + +### Verbos canonicos (allowlist) + +Lista no exhaustiva pero cubre la mayoria. Anadir aqui (y al validator en `apps/registry_mcp/naming.go`) cuando se introduzca un verbo nuevo recurrente. + +`get, set, list, find, search, show, read, load, fetch, scan, query, lookup, parse, format, encode, decode, marshal, unmarshal, serialize, deserialize, validate, check, ensure, verify, audit, diagnose, test, match, filter, map, reduce, sort, group, count, sum, aggregate, compute, calculate, score, rank, cluster, classify, detect, init, create, make, build, generate, scaffold, install, setup, configure, register, add, insert, append, prepend, update, upsert, modify, edit, patch, replace, delete, remove, clear, drop, prune, clean, copy, move, rename, sync, clone, extract, inject, import, export, send, post, put, call, dispatch, exec, run, launch, start, stop, kill, restart, redeploy, deploy, open, close, connect, disconnect, login, logout, authenticate, enable, disable, toggle, lock, unlock, propose, promote, deprecate, approve, reject, emit, render, draw, paint, serve, host, pull, push, checkout, commit, tag, merge, rebase, watch, monitor, observe, log, trace, profile, benchmark, snapshot, backup, restore, archive, compress, decompress, hash, encrypt, decrypt, sign, taskkill, recopile, vault, propose, apply, gather, collect, fold, head, tail, take, drop, slice, chunk, batch, debounce, throttle, retry, await, sleep, ping, kill, prime, warm, refresh, invalidate, reload, reset, rollback, fork, spawn, daemon, observe, plot, draw, capture, replay, recopilate` + +### Excepciones + +- **Operadores matematicos/estadisticos** ampliamente reconocidos por acronimo: `sma`, `ema`, `rsi`, `vwap`, `adx`. Validator hace allowlist explicita. +- **Tipos** (entity_type `type`): no requieren verbo. Validator lo salta cuando `kind=type`. +- **Components** (`kind: component`): nombre describe artefacto UI (`button_primary`, `chat_panel`). Permite forma `_`. Validator salta el check de verbo si `kind=component`. + +### Validator + +`mcp__registry__fn_create_function` ejecuta el validator antes de escribir archivos. Rechaza con error si: +- name no es snake_case. +- name no contiene verbo (excepto component/type). +- domain no esta en lista canonica. + +Error tipico: +``` +naming: name "slice" lacks action verb. Add verb prefix/suffix (e.g. filter_slice, slice_window). See .claude/rules/ids_naming.md. +naming: domain "bizops" not in canonical list (core, infra, finance, ...). Add it to CLAUDE.md and rules first. +``` diff --git a/.claude/scripts/append_fn_to_memory.sh b/.claude/scripts/append_fn_to_memory.sh new file mode 100755 index 00000000..b0652456 --- /dev/null +++ b/.claude/scripts/append_fn_to_memory.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Append a one-liner [[fn_id]] — purpose to MEMORY.md after fn-constructor +# creates a new registry function. Idempotent: skips if id already present. +# Used by /fn_claude step 5b (issue 0087, pieza 6). +# +# Usage: append_fn_to_memory.sh "" + +set -euo pipefail + +FN_ID="${1:-}" +PURPOSE="${2:-}" + +if [ -z "$FN_ID" ] || [ -z "$PURPOSE" ]; then + echo "usage: append_fn_to_memory.sh " >&2 + exit 2 +fi + +MEM_DIR="${CLAUDE_MEMORY_DIR:-/home/lucas/.claude/projects/-home-lucas-fn-registry/memory}" +MEM_FILE="$MEM_DIR/MEMORY.md" + +[ -d "$MEM_DIR" ] || { echo "memory dir missing: $MEM_DIR" >&2; exit 1; } +[ -f "$MEM_FILE" ] || { echo "MEMORY.md missing: $MEM_FILE" >&2; exit 1; } + +# Per-function reference file slug +SLUG="reference_fn_${FN_ID}.md" +REF_FILE="$MEM_DIR/$SLUG" + +# Idempotency: if already linked in MEMORY.md, exit 0 +if grep -qF "[fn-$FN_ID]" "$MEM_FILE" 2>/dev/null; then + echo "already in MEMORY.md: $FN_ID" + exit 0 +fi + +# 1. Create reference memory file +cat > "$REF_FILE" <> "$MEM_FILE" + +echo "appended: $FN_ID -> $MEM_FILE" diff --git a/CHANGELOG.md b/CHANGELOG.md index 121dd009..2a1a05f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,35 @@ Para contexto detallado del trabajo diario ver `docs/diary/`. Para decisiones ar ## [Unreleased] +## 2026-05-16 + +### Added + +- **Iconos `.ico` Windows para apps C++** — 11 apps GUI (`chart_demo`, `dag_engine_ui`, `data_factory`, `graph_explorer`, `navegator_dashboard`, `odr_console`, `primitives_gallery`, `registry_dashboard`, `shaders_lab`, `text_editor_smoke`, `altsnap_jitter_test`) ahora tienen icono propio en el `.exe` y en `` desplegado. + - Glyphs: **Phosphor Icons** (`fill` weight), clonado en `sources/phosphor-core/` (1512 SVGs disponibles). Cada app usa un `accent_hex` distinto (Tailwind 500-700) para distinguirse en taskbar/desktop. + - Mapping inicial en `dev/gen_app_icons.py` (script reproducible). Cada `.ico` multi-resolucion (16/24/32/48/64/128/256). + - Wiring CMake: `cpp/CMakeLists.txt:1-5` declara `LANGUAGES C CXX RC` en WIN32; `add_imgui_app` macro detecta `/appicon.ico` y genera `_appicon.rc` enlazado via `windres` (toolchain `cpp/toolchains/mingw-w64.cmake`). + - Nueva funcion del registry: `generate_app_icon_py_infra` (`python/functions/infra/generate_app_icon.{py,md}`). Toma `phosphor_icon_name + accent_hex + out_ico_path` y exporta `.ico` multi-res. Tags: `cpp-windows`, `icon`, `phosphor`. + - Convencion documentada en `.claude/rules/cpp_apps.md §11`. + +- **C++ framework — Alt+RMB resize / Alt+LMB move anywhere** (`cpp/framework/app_base.cpp`). WndProc subclass detecta `WM_RBUTTONDOWN`/`WM_LBUTTONDOWN` con `GetAsyncKeyState(VK_MENU) & 0x8000`, `ReleaseCapture` + `PostMessage(WM_SYSCOMMAND, SC_SIZE|dir | SC_MOVE|HTCAPTION)`. Modal nativo, cero jitter automatico via gate sizemove existente. Aplica a main + cada viewport flotante (subclass per-frame). +- **C++ framework — multi-HWND subclass** para anti-jitter. `g_subclassed` ahora `unordered_map`, scan per-frame en `pio.Viewports` instala subclass en cada HWND nuevo, `prune_dead_subclassed()` con `IsWindow`, `uninstall_sizemove_subclass_all()` al exit. Fix del temblor en paneles flotantes (no solo el main HWND). +- **C++ framework — iconified survival** de paneles flotantes. Antes `glfwWaitEvents+continue` paraba el frame loop entero al minimizar el main → secondary viewports congelados/ocultos. Ahora detecta secondary viewports y fall-through al frame normal si existen; solo duerme cuando no hay flotantes. +- **C++ framework — `fn::internal::*` test observability**. `sizemove_enter_count()`, `alt_rmb_resize_count()`, `alt_lmb_move_count()`, `rbuttondown_seen_count()`, `set_force_alt_for_test(bool)`. Counters monotonicos zero-cost, modo test salta `PostMessage SC_SIZE/SC_MOVE` para no atrapar al harness en modal. +- **`apps/altsnap_jitter_test/`** — extendido a 6 phases (p1 sync, p2 main HWND modal, p3 secondary HWND modal, p4 iconify+restore preserva floating, p5 Alt+RMB consumed, p6 Alt+LMB consumed). Todas PASS en Windows. +- **`redeploy_all_cpp_apps_bash_pipelines`** — pipeline nuevo `bash/functions/pipelines/redeploy_all_cpp_apps.sh` que cross-compila todo el arbol `cpp/` en un solo cmake pass + redeploy de cada `.exe` al Desktop. Filtro opcional por substring de nombre. Tolerante a fallos (build best-effort, summary OK/SKIPPED/FAILED). Tags: `cpp, windows, deploy, redeploy, bulk, cpp-windows`. Composicion: `build_cpp_windows_bash_infra` + loop `taskkill.exe` + `deploy_cpp_exe_to_windows_bash_infra`. + +### Changed + +- **`io.ConfigWindowsMoveFromTitleBarOnly = true`** en `fn::run_app`. Floating panels (viewport secundario = OS window borderless con UNA ventana ImGui rellenandolo) ahora respetan "solo header arrastra" como las decoradas. Fix del drag-anywhere-sin-alt en panel flotante. Alt+LMB anywhere sigue funcionando (subclass consume antes que ImGui). +- **`resolve_cpp_app_dir_bash_infra` v1.1.0** — ahora busca apps tambien en `apps//` (canonical issue 0096) ademas de `cpp/apps//` (legacy) y `projects/*/apps//`. Fix retroactivo: `./fn run compile_cpp_app ` fallaba para apps en el layout canonical (ej. `dag_engine_ui`). Deduccion desde CWD tambien actualizada. Helper interno `_list_cpp_apps`. + +### Notes + +- Apps C++ redesplegadas via `redeploy_all_cpp_apps`: 12 OK / 1 SKIP (`data_factory` sin .exe target) / 0 FAILED. Todas tienen los fixes del framework activos. +- ImGui_ImplGlfw subclassea el HWND DESPUES que nuestro framework. ImGui captura nuestro WndProc como `PrevWndProc` y chainea via `CallWindowProc`, asi que el subclass nuestro sigue recibiendo TODOS los mensajes en el orden correcto. NO re-subclassear despues de ImGui init (provoca recursion infinita por cycle: `our_proc -> orig=imgui_proc -> imgui_proc -> prev=our_proc -> ...`). +- Pre-existing build break en `cpp/tests/test_llm_anthropic.cpp` + `cpp/tests/test_graph_icons.cpp` por uso de `setenv()` que no existe en mingw-w64. NO bloquea `redeploy_all_cpp_apps` (build best-effort). Candidato a guard `#ifdef _WIN32` con `_putenv_s` o skip cross-compile. No introducido por esta sesion. + ## 2026-05-14 ### Added diff --git a/altsnap_jitter_test.log b/altsnap_jitter_test.log new file mode 100644 index 00000000..bda6ca52 --- /dev/null +++ b/altsnap_jitter_test.log @@ -0,0 +1,4 @@ +[2026-05-15 23:51:43.764] [INFO] app start: altsnap_jitter_test +[2026-05-15 23:51:44.017] [INFO] app exit +[2026-05-15 23:52:47.933] [INFO] app start: altsnap_jitter_test +[2026-05-15 23:52:48.135] [INFO] app exit diff --git a/apps/dag_engine/README.md b/apps/dag_engine/README.md index 13715362..6dac28a1 100644 --- a/apps/dag_engine/README.md +++ b/apps/dag_engine/README.md @@ -155,9 +155,10 @@ handler_on: | `name` | string | (obligatorio) | Identificador del step dentro del DAG. | | `id` | string | "" | Override del id auto-generado. | | `description` | string | "" | Texto libre. | -| `command` | string | "" | Comando shell (mutuamente excluyente con `script`). | +| `command` | string | "" | Comando shell (mutuamente excluyente con `script`/`function`). | | `script` | string | "" | Bloque heredoc. Util para Python/Lua inline. | -| `args` | list[string] | [] | Args extra para `command`. | +| `function` | string | "" | ID de funcion del registry (ej `audit_capability_groups_go_infra`). Si set, executor invoca `${FN_REGISTRY_ROOT}/fn run ` y captura `function_id` en `dag_step_results`. Mutuamente exclusivo con `command`/`script`; si convive, gana `function`. | +| `args` | list[string] | [] | Args extra para `command` o para la `function`. | | `shell` | string | hereda | Override del shell. | | `dir` / `working_dir` | string | hereda | Working dir para este step. | | `depends` | list[string] | [] | Steps que deben terminar OK antes. Si vacio + `type:graph`, arranca en paralelo. | @@ -170,6 +171,28 @@ handler_on: | `output` | string | "" | Nombre de variable donde guardar stdout (consumible por dependientes). | | `tags` | list[string] | [] | Tags por step (UI). | +### Function steps (coherencia con el registry) + +Un DAG idiomatico llama funciones del registry, no scripts ad-hoc. Cada step `function:` queda trazado en `call_monitor.calls` por el hook PostToolUse del agente y en `dag_step_results.function_id` del propio dag_engine — el bucle reactivo (issue 0085) tiene visibilidad end-to-end. + +```yaml +steps: + - name: audit_capabilities + function: audit_capability_groups_go_infra + args: ["--json"] + description: "Audita drift entre tags de capability group y paginas madre" +``` + +Ventajas vs `command: ./fn run ...`: + +- `function_id` se persiste como columna dedicada en `dag_step_results` (filtrable, agrupable). +- El frontend `dag_engine_ui` muestra badge + panel lateral con `uses_functions` (subfunciones que el step va a usar transitivamente). +- API: `GET /api/functions/{id}` devuelve `{id, name, description, signature, purity, domain, lang, uses_functions[], uses_types[]}` leyendo `registry.db` read-only. La UI consume este endpoint al expandir un step. +- Validator regex en `dag_validate`: `^[a-z0-9_]+_[a-z]+_[a-z]+$`. ID invalido = error. +- Variables de entorno: `FN_REGISTRY_ROOT` (default `/home/lucas/fn_registry`) localiza el binario `fn`. Override con `FN_BIN=/path/al/fn`. + +Ejemplo completo: `~/.dagu/dags/example-fn-call.yaml`. + ### Cron schedule 5 campos clasicos: `min hour dom mon dow`. Ejemplos: diff --git a/apps/dag_engine/api.go b/apps/dag_engine/api.go index 5b8abab2..ff58080d 100644 --- a/apps/dag_engine/api.go +++ b/apps/dag_engine/api.go @@ -15,6 +15,9 @@ func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, h mux.HandleFunc("GET /api/runs", handleListRuns(executor)) mux.HandleFunc("GET /api/runs/{id}", handleGetRun(executor)) + // Function lookup proxy a registry.db (read-only). + mux.HandleFunc("GET /api/functions/{id}", handleGetFunction()) + mux.HandleFunc("POST /api/scheduler/start", handleSchedulerStart(scheduler)) mux.HandleFunc("POST /api/scheduler/stop", handleSchedulerStop(scheduler)) mux.HandleFunc("GET /api/scheduler/status", handleSchedulerStatus(scheduler)) diff --git a/apps/dag_engine/dags_migrated/example.yaml b/apps/dag_engine/dags_migrated/example.yaml deleted file mode 100644 index 5d1ea9f9..00000000 --- a/apps/dag_engine/dags_migrated/example.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# Example Dagu DAG -# This is a simple example workflow - -name: example -description: Example workflow to demonstrate Dagu capabilities - -schedule: - # Run every day at 9:00 AM - - "0 9 * * *" - -steps: - - name: hello - command: echo "Hello from Dagu!" - - - name: list_files - command: ls -la /home/lucas/dagu/scripts - depends: - - hello - - - name: date - command: date - depends: - - hello diff --git a/apps/dag_engine/dags_migrated/example_lineage_tracking.yaml b/apps/dag_engine/dags_migrated/example_lineage_tracking.yaml deleted file mode 100644 index 6e1934aa..00000000 --- a/apps/dag_engine/dags_migrated/example_lineage_tracking.yaml +++ /dev/null @@ -1,178 +0,0 @@ -name: example_lineage_tracking -description: | - Ejemplo completo de pipeline con lineage tracking usando marquez-cli. - - Este DAG demuestra: - - Generación de Run ID único - - Eventos START, RUNNING, COMPLETE - - Tracking de inputs/outputs en cada paso - - Manejo de errores con evento FAIL - -tags: - - example - - lineage - - marquez - -schedule: - - "0 */6 * * *" # Cada 6 horas - -env: - - MARQUEZ_URL: http://localhost:5000 - - MARQUEZ_NAMESPACE: automatic-process - - JOB_NAME: example_lineage_tracking - - RUN_ID: "" - -steps: - # PASO 0: Generar Run ID único para todo el pipeline - - name: init_run_id - description: Generate unique Run ID for this execution - command: | - RUN_ID=$(uuidgen) - echo "RUN_ID=$RUN_ID" >> $DAGU_ENV - echo "Generated Run ID: $RUN_ID" - - # PASO 1: START event - - name: start_run - description: Send START event to Marquez - command: | - marquez-cli run start \ - -job $JOB_NAME \ - -run-id $RUN_ID \ - -namespace $MARQUEZ_NAMESPACE \ - -inputs "api://jsonplaceholder.typicode.com/users" - - echo "✓ Run started with ID: $RUN_ID" - depends: - - init_run_id - - # PASO 2: Extract - Fetch data from API - - name: extract_data - description: Fetch data from external API - command: | - echo "Fetching data from API..." - curl -s https://jsonplaceholder.typicode.com/users > /tmp/lineage_users.json - - marquez-cli run running \ - -job $JOB_NAME \ - -run-id $RUN_ID \ - -namespace $MARQUEZ_NAMESPACE \ - -inputs "api://jsonplaceholder.typicode.com/users" \ - -outputs "file:///tmp/lineage_users.json" - - echo "✓ Data extracted: $(cat /tmp/lineage_users.json | jq '. | length') records" - depends: - - start_run - - # PASO 3: Transform - Clean and transform data - - name: transform_data - description: Transform and clean the data - command: | - echo "Transforming data..." - jq '[.[] | {email: .email, name: .name, company: .company.name}]' \ - /tmp/lineage_users.json > /tmp/lineage_users_clean.json - - marquez-cli run running \ - -job $JOB_NAME \ - -run-id $RUN_ID \ - -namespace $MARQUEZ_NAMESPACE \ - -inputs "file:///tmp/lineage_users.json" \ - -outputs "file:///tmp/lineage_users_clean.json" - - echo "✓ Data transformed: $(cat /tmp/lineage_users_clean.json | jq '. | length') records" - depends: - - extract_data - - # PASO 4: Load - Save to PostgreSQL - - name: load_data - description: Load data to PostgreSQL - command: | - echo "Loading data to PostgreSQL..." - - # Crear tabla si no existe - psql -h localhost -p 5434 -U postgres -d postgres -c " - CREATE TABLE IF NOT EXISTS lineage_example ( - email TEXT, - name TEXT, - company TEXT, - loaded_at TIMESTAMP DEFAULT NOW() - ); - " - - # Truncar tabla - psql -h localhost -p 5434 -U postgres -d postgres -c "TRUNCATE TABLE lineage_example;" - - # Cargar datos - jq -r '.[] | [.email, .name, .company] | @csv' /tmp/lineage_users_clean.json | \ - psql -h localhost -p 5434 -U postgres -d postgres -c " - COPY lineage_example (email, name, company) FROM STDIN WITH CSV; - " - - RECORD_COUNT=$(psql -h localhost -p 5434 -U postgres -d postgres -t -c "SELECT COUNT(*) FROM lineage_example;") - - marquez-cli run running \ - -job $JOB_NAME \ - -run-id $RUN_ID \ - -namespace $MARQUEZ_NAMESPACE \ - -inputs "file:///tmp/lineage_users_clean.json" \ - -outputs "postgres://localhost:5434/postgres/public/lineage_example" - - echo "✓ Data loaded: $(echo $RECORD_COUNT | xargs) records" - depends: - - transform_data - - # PASO 5: COMPLETE event - - name: complete_run - description: Mark run as completed in Marquez - command: | - marquez-cli run complete \ - -job $JOB_NAME \ - -run-id $RUN_ID \ - -namespace $MARQUEZ_NAMESPACE \ - -inputs "api://jsonplaceholder.typicode.com/users" \ - -outputs "postgres://localhost:5434/postgres/public/lineage_example" - - echo "✓ Run completed successfully: $RUN_ID" - echo "" - echo "Verify lineage at: http://localhost:3001" - echo "Or run: marquez-cli lineage -name 'postgres://localhost:5434/postgres/public/lineage_example'" - depends: - - load_data - - # PASO 6: Cleanup temporary files - - name: cleanup - description: Remove temporary files - command: | - rm -f /tmp/lineage_users.json /tmp/lineage_users_clean.json - echo "✓ Temporary files cleaned" - depends: - - complete_run - -# Handler para errores -handlers: - failure: - - name: mark_as_failed - command: | - echo "❌ Pipeline failed, marking run as FAILED in Marquez" - - if [ -n "$RUN_ID" ]; then - marquez-cli run fail \ - -job $JOB_NAME \ - -run-id $RUN_ID \ - -namespace $MARQUEZ_NAMESPACE - - echo "✓ Run marked as FAILED: $RUN_ID" - else - echo "⚠ No RUN_ID found, skipping FAIL event" - fi - - success: - - name: notify_success - command: | - echo "🎉 Pipeline completed successfully!" - echo "Run ID: $RUN_ID" - echo "View lineage: http://localhost:3001" - -# Configuración de logs -logCleanup: - enabled: true - retentionDays: 7 diff --git a/apps/dag_engine/dags_migrated/fn_backup.yaml b/apps/dag_engine/dags_migrated/fn_backup.yaml index 69ebac8d..f5a37673 100644 --- a/apps/dag_engine/dags_migrated/fn_backup.yaml +++ b/apps/dag_engine/dags_migrated/fn_backup.yaml @@ -1,11 +1,12 @@ name: fn_backup -description: Backup diario de fn_registry (registry.db + operations.db + vaults) +description: Backup diario de fn_registry (registry.db + operations.db + vaults) via funcion del registry schedule: - "0 3 * * *" +tags: [backup, registry, daily] + env: - - FN_REGISTRY_ROOT: /home/lucas/fn_registry - BACKUP_ROOT: /home/lucas/backups/fn_registry steps: @@ -13,9 +14,13 @@ steps: command: mkdir -p ${BACKUP_ROOT} - name: run_backup_all - command: bash /home/lucas/fn_registry/bash/functions/pipelines/backup_all.sh ${BACKUP_ROOT} + description: "Snapshot atomico de registry.db + operations.db + vaults con retention 7/4/12" + function: backup_all_bash_pipelines + args: ["${BACKUP_ROOT}"] continue_on: exit_code: [4] + depends: [ensure_dirs] - name: report_status command: bash -c 'ls -lh ${BACKUP_ROOT}/registry/daily.0 ${BACKUP_ROOT}/operations/*/daily.0 2>/dev/null | tail -20' + depends: [run_backup_all] diff --git a/apps/dag_engine/dags_migrated/test_claude_access.yaml b/apps/dag_engine/dags_migrated/test_claude_access.yaml deleted file mode 100644 index 875d6e96..00000000 --- a/apps/dag_engine/dags_migrated/test_claude_access.yaml +++ /dev/null @@ -1,23 +0,0 @@ -name: test_claude_access -description: Test workflow created by Claude to verify access - -tags: - - test - - claude - -steps: - - name: verify_access - command: echo "✓ Claude tiene acceso completo para gestionar tus pipelines de Dagu!" - - - name: show_info - command: | - echo "Usuario: $(whoami)" - echo "Fecha: $(date)" - echo "Directorio: $(pwd)" - depends: - - verify_access - - - name: cleanup - command: echo "Pipeline de prueba completado exitosamente" - depends: - - show_info diff --git a/apps/dag_engine/executor.go b/apps/dag_engine/executor.go index a14a90ec..dec7a327 100644 --- a/apps/dag_engine/executor.go +++ b/apps/dag_engine/executor.go @@ -156,22 +156,41 @@ func (e *Executor) ExecuteDAG(ctx context.Context, dagPath string, trigger strin func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDefinition, step core.DagStep, daguEnvPath string, outputs map[string]string, mu *sync.Mutex) error { stepID := generateID() now := time.Now() + + // Resolve command source: function (registry) takes precedence over command/script. + var command string + var stepFunctionID string + if step.Function != "" { + stepFunctionID = step.Function + fnBin := os.Getenv("FN_BIN") + if fnBin == "" { + root := os.Getenv("FN_REGISTRY_ROOT") + if root == "" { + root = "/home/lucas/fn_registry" + } + fnBin = root + "/fn" + } + parts := []string{fnBin, "run", step.Function} + parts = append(parts, step.Args...) + command = strings.Join(parts, " ") + } else if step.Command != "" { + command = step.Command + } else if step.Script != "" { + command = step.Script + } + e.store.InsertStepResult(&store.DagStepResult{ - ID: stepID, - RunID: runID, - StepName: stepName(step), - Status: "running", - StartedAt: &now, + ID: stepID, + RunID: runID, + StepName: stepName(step), + FunctionID: stepFunctionID, + Status: "running", + StartedAt: &now, }) // Build environment. env := buildStepEnv(dag, step, daguEnvPath, outputs) - // Determine command. - command := step.Command - if command == "" && step.Script != "" { - command = step.Script - } if command == "" { e.store.UpdateStepResult(stepID, "skipped", 0, "", "", nil, 0, "no command or script") return nil diff --git a/apps/dag_engine/handlers_dags.go b/apps/dag_engine/handlers_dags.go index 8998d6de..22634ca2 100644 --- a/apps/dag_engine/handlers_dags.go +++ b/apps/dag_engine/handlers_dags.go @@ -30,10 +30,10 @@ func handleGetDag(executor *Executor) http.HandlerFunc { runs, _, _ := executor.store.ListRuns(dag.Name, 10, 0) resp := map[string]interface{}{ - "info": info, - "dag": dag, - "validation": validation, - "runs": runs, + "info": info, + "dag": dag, + "validation": validation, + "recent_runs": runs, } writeJSON(w, http.StatusOK, resp) } diff --git a/apps/dag_engine/store/store.go b/apps/dag_engine/store/store.go index df17c500..998587ac 100644 --- a/apps/dag_engine/store/store.go +++ b/apps/dag_engine/store/store.go @@ -2,15 +2,43 @@ package store import ( "database/sql" - _ "embed" + "embed" "fmt" + "io/fs" + "sort" + "strings" "time" _ "github.com/mattn/go-sqlite3" ) -//go:embed migrations/001_init.sql -var migrationSQL string +//go:embed migrations/*.sql +var migrationsFS embed.FS + +// applyMigrations executes every embedded migrations/*.sql in order. +// Each statement is idempotent (IF NOT EXISTS / ADD COLUMN). Duplicate-column +// errors from re-running ALTER TABLE ADD COLUMN are tolerated. +func applyMigrations(conn *sql.DB) error { + files, err := fs.Glob(migrationsFS, "migrations/*.sql") + if err != nil { + return err + } + sort.Strings(files) + for _, f := range files { + b, err := migrationsFS.ReadFile(f) + if err != nil { + return fmt.Errorf("%s: read: %w", f, err) + } + if _, err := conn.Exec(string(b)); err != nil { + if strings.Contains(err.Error(), "duplicate column") || + strings.Contains(err.Error(), "already exists") { + continue + } + return fmt.Errorf("%s: %w", f, err) + } + } + return nil +} // DB wraps a SQLite connection for DAG run persistence. type DB struct { @@ -24,7 +52,7 @@ func Open(path string) (*DB, error) { if err != nil { return nil, fmt.Errorf("store: open %s: %w", path, err) } - if _, err := conn.Exec(migrationSQL); err != nil { + if err := applyMigrations(conn); err != nil { conn.Close() return nil, fmt.Errorf("store: migrate: %w", err) } @@ -132,6 +160,7 @@ type DagStepResult struct { ID string `json:"id"` RunID string `json:"run_id"` StepName string `json:"step_name"` + FunctionID string `json:"function_id,omitempty"` Status string `json:"status"` ExitCode int `json:"exit_code"` Stdout string `json:"stdout,omitempty"` @@ -154,9 +183,9 @@ func (db *DB) InsertStepResult(r *DagStepResult) error { finishedAt = &s } _, err := db.conn.Exec( - `INSERT INTO dag_step_results (id, run_id, step_name, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - r.ID, r.RunID, r.StepName, r.Status, r.ExitCode, r.Stdout, r.Stderr, + `INSERT INTO dag_step_results (id, run_id, step_name, function_id, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + r.ID, r.RunID, r.StepName, r.FunctionID, r.Status, r.ExitCode, r.Stdout, r.Stderr, startedAt, finishedAt, r.DurationMs, r.Error, ) return err @@ -179,7 +208,7 @@ func (db *DB) UpdateStepResult(id, status string, exitCode int, stdout, stderr s // ListStepResults returns all step results for a given run. func (db *DB) ListStepResults(runID string) ([]DagStepResult, error) { rows, err := db.conn.Query( - `SELECT id, run_id, step_name, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error + `SELECT id, run_id, step_name, function_id, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error FROM dag_step_results WHERE run_id=? ORDER BY started_at ASC`, runID, ) if err != nil { @@ -191,7 +220,7 @@ func (db *DB) ListStepResults(runID string) ([]DagStepResult, error) { for rows.Next() { var r DagStepResult var startedAt, finishedAt sql.NullString - if err := rows.Scan(&r.ID, &r.RunID, &r.StepName, &r.Status, &r.ExitCode, + if err := rows.Scan(&r.ID, &r.RunID, &r.StepName, &r.FunctionID, &r.Status, &r.ExitCode, &r.Stdout, &r.Stderr, &startedAt, &finishedAt, &r.DurationMs, &r.Error); err != nil { return nil, err } diff --git a/bash/functions/cybersecurity/scan_secrets_in_dirty.sh b/bash/functions/cybersecurity/scan_secrets_in_dirty.sh index aff8a79c..d8b8a7c8 100644 --- a/bash/functions/cybersecurity/scan_secrets_in_dirty.sh +++ b/bash/functions/cybersecurity/scan_secrets_in_dirty.sh @@ -6,16 +6,21 @@ scan_secrets_in_dirty() { local repo_dir="${1:-.}" - if [[ ! -d "$repo_dir/.git" ]]; then + # Accept both regular repos (.git is a directory) and worktrees (.git is a + # file containing "gitdir: ..." pointer). + if [[ ! -d "$repo_dir/.git" && ! -f "$repo_dir/.git" ]]; then echo "scan_secrets_in_dirty: '$repo_dir' no es un repo git" >&2 return 1 fi # Listar archivos modificados o nuevos (excluyendo borrados) - # y filtrar por patron de secret en el nombre del archivo + # y filtrar por patron de secret en el nombre del archivo. + # Excluye extensiones de codigo (sh/go/py/ts/md/etc) para no marcar el + # propio scanner ni docs que hablen de "secret"/"token". git -C "$repo_dir" status --porcelain \ | awk '{print $NF}' \ | grep -E '(^|/)(\.env(\..*)?$|.*credentials.*|.*\.key$|.*\.pem$|id_rsa.*|.*secret.*|.*token.*\.txt$)' \ + | grep -Ev '\.(sh|go|py|ts|tsx|js|jsx|md|rs|cpp|h|hpp|c|java|rb|html|css)$' \ || true } diff --git a/bash/functions/infra/git_hook_audit_app_drift.sh b/bash/functions/infra/git_hook_audit_app_drift.sh index 18dba3de..a8920cb6 100755 --- a/bash/functions/infra/git_hook_audit_app_drift.sh +++ b/bash/functions/infra/git_hook_audit_app_drift.sh @@ -17,7 +17,9 @@ git_hook_audit_app_drift() { echo "ERROR: repo_dir required" >&2 return 2 fi - if [[ ! -d "$repo_dir/.git" ]]; then + # Accept both regular repos (.git is a directory) and worktrees (.git is a + # file containing "gitdir: ..." pointer). + if [[ ! -d "$repo_dir/.git" && ! -f "$repo_dir/.git" ]]; then echo "ERROR: $repo_dir is not a git repo" >&2 return 2 fi diff --git a/bash/functions/infra/resolve_cpp_app_dir.md b/bash/functions/infra/resolve_cpp_app_dir.md index 863af07e..b98846ea 100644 --- a/bash/functions/infra/resolve_cpp_app_dir.md +++ b/bash/functions/infra/resolve_cpp_app_dir.md @@ -3,11 +3,11 @@ name: resolve_cpp_app_dir kind: function lang: bash domain: infra -version: "1.0.0" +version: "1.1.0" purity: impure signature: "resolve_cpp_app_dir(app_name?: string) -> stdout: app_name\tapp_dir" -description: "Resuelve el nombre y directorio absoluto de una app C++ del registry. Sin arg deduce desde CWD si esta dentro de cpp/apps// o projects/*/apps//. Con arg busca en ambas ubicaciones. Imprime 'TAB' en stdout, exit 0; si no resuelve, lista apps disponibles en stderr y sale con exit 1." -tags: [cpp, resolve, app, directory, infra] +description: "Resuelve el nombre y directorio absoluto de una app C++ del registry. Sin arg deduce desde CWD si esta dentro de apps//, cpp/apps// o projects/*/apps//. Con arg busca en las tres ubicaciones (apps/ canonical issue 0096 primero, luego cpp/apps/ legacy, luego projects/*/apps/). Imprime 'TAB' en stdout, exit 0; si no resuelve, lista apps disponibles en stderr y sale con exit 1." +tags: [cpp, resolve, app, directory, infra, cpp-windows] uses_functions: [] uses_types: [] returns: [] @@ -20,7 +20,7 @@ test_file_path: "" file_path: "bash/functions/infra/resolve_cpp_app_dir.sh" params: - name: app_name - desc: "Nombre de la app C++ a resolver (opcional). Sin arg se deduce desde el directorio actual si estamos dentro de cpp/apps// o projects/*/apps//." + desc: "Nombre de la app C++ a resolver (opcional). Sin arg se deduce desde el directorio actual si estamos dentro de apps//, cpp/apps// o projects/*/apps//." output: "Una linea TAB-separada '\\t' en stdout. En caso de error imprime ayuda a stderr y sale con exit 1." --- @@ -44,4 +44,13 @@ APP_DIR="$(echo "$resolved" | cut -f2)" ## Notas -Busca en orden: primero `$ROOT/cpp/apps/`, luego `$ROOT/projects/*/apps/` (primer match gana). Si ninguna ruta existe, imprime lista de apps disponibles (con prefijo de ubicacion) en stderr y sale con exit 1. Sourceable o ejecutable directamente. +Busca en orden: +1. `$ROOT/apps/` con `CMakeLists.txt` — layout canonical post-issue 0096. +2. `$ROOT/cpp/apps/` — legacy pre-issue 0096. +3. `$ROOT/projects/*/apps/` — apps de un proyecto (primer match gana). + +Si ninguna ruta existe, imprime lista de apps disponibles (con prefijo de ubicacion) en stderr y sale con exit 1. Sourceable o ejecutable directamente. Helper interno `_list_cpp_apps` evita duplicar codigo en los paths de error. + +### Growth log + +- v1.1.0 (2026-05-16) — busca tambien en `apps//` (canonical issue 0096). Antes solo cubria `cpp/apps//` y `projects/*/apps//`, lo que hacia que `./fn run compile_cpp_app ` fallara para apps movidas al layout canonical (ej. `dag_engine_ui`). diff --git a/bash/functions/infra/resolve_cpp_app_dir.sh b/bash/functions/infra/resolve_cpp_app_dir.sh index e8c79eb7..5c9d39a5 100644 --- a/bash/functions/infra/resolve_cpp_app_dir.sh +++ b/bash/functions/infra/resolve_cpp_app_dir.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # resolve_cpp_app_dir — Resuelve nombre y directorio absoluto de una app C++ del registry. -# Sin arg: deduce desde CWD si esta dentro de cpp/apps// o projects/*/apps//. -# Con arg: usa el nombre directamente y busca en ambas ubicaciones. +# Sin arg: deduce desde CWD si esta dentro de apps//, cpp/apps// o projects/*/apps//. +# Con arg: usa el nombre directamente y busca en las tres ubicaciones. # Salida: "\t" en stdout (TAB separado), exit 0. # Error: lista apps disponibles en stderr + exit 1. @@ -9,18 +9,28 @@ resolve_cpp_app_dir() { local app_arg="${1:-}" local root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}" + _list_cpp_apps() { + ls "$root/apps/" 2>/dev/null | sed 's/^/ apps\//' + ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//' + for proj in "$root"/projects/*/apps/; do + ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|" + done + } + # --- Deducir desde CWD si no hay argumento --- if [ -z "$app_arg" ]; then local cwd cwd="$(pwd)" case "$cwd" in + "$root"/apps/*/|"$root"/apps/*) + local rel="${cwd#"$root/apps/"}" + app_arg="${rel%%/*}" + ;; "$root"/cpp/apps/*/|"$root"/cpp/apps/*) - # Extraer primer segmento tras cpp/apps/ local rel="${cwd#"$root/cpp/apps/"}" app_arg="${rel%%/*}" ;; "$root"/projects/*/apps/*/|"$root"/projects/*/apps/*) - # Extraer primer segmento tras la ultima /apps/ local rel="${cwd#"$root/projects/"}" rel="${rel#*/apps/}" app_arg="${rel%%/*}" @@ -33,12 +43,7 @@ resolve_cpp_app_dir() { echo "ERROR: no se pudo deducir la app desde el directorio actual." >&2 echo "" >&2 echo "Apps disponibles:" >&2 - { - ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//' - for proj in "$root"/projects/*/apps/; do - ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|" - done - } >&2 + _list_cpp_apps >&2 echo "" >&2 echo "Uso: resolve_cpp_app_dir " >&2 return 1 @@ -47,12 +52,17 @@ resolve_cpp_app_dir() { # --- Buscar directorio real --- local app_dir="" - # Primero: cpp/apps/ - if [ -d "$root/cpp/apps/$app_arg" ]; then + # Primero (issue 0096 canonical): apps/ + if [ -d "$root/apps/$app_arg" ] && [ -f "$root/apps/$app_arg/CMakeLists.txt" ]; then + app_dir="$root/apps/$app_arg" + fi + + # Segundo (legacy): cpp/apps/ + if [ -z "$app_dir" ] && [ -d "$root/cpp/apps/$app_arg" ]; then app_dir="$root/cpp/apps/$app_arg" fi - # Segundo: projects/*/apps/ (primer match) + # Tercero: projects/*/apps/ (primer match) if [ -z "$app_dir" ]; then for cand in "$root"/projects/*/apps/"$app_arg"; do if [ -d "$cand" ]; then @@ -63,15 +73,10 @@ resolve_cpp_app_dir() { fi if [ -z "$app_dir" ]; then - echo "ERROR: no se encuentra app '$app_arg' en cpp/apps/ ni en projects/*/apps/" >&2 + echo "ERROR: no se encuentra app '$app_arg' en apps/, cpp/apps/ ni en projects/*/apps/" >&2 echo "" >&2 echo "Apps disponibles:" >&2 - { - ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//' - for proj in "$root"/projects/*/apps/; do - ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|" - done - } >&2 + _list_cpp_apps >&2 return 1 fi diff --git a/bash/functions/pipelines/fn_sync_with_pass.md b/bash/functions/pipelines/fn_sync_with_pass.md new file mode 100644 index 00000000..be2eda58 --- /dev/null +++ b/bash/functions/pipelines/fn_sync_with_pass.md @@ -0,0 +1,56 @@ +--- +name: fn_sync_with_pass +kind: pipeline +lang: bash +domain: pipelines +version: "1.0.0" +purity: impure +signature: "fn_sync_with_pass [status|locations|...]" +description: "Wrapper de fn sync que lee credenciales del password-store pass y exporta FN_REGISTRY_API y REGISTRY_API_TOKEN antes de invocar el CLI. Evita persistir secretos en ~/.zshrc." +tags: [sync, registry, pass, gpg, launcher] +uses_functions: + - pass_get_bash_infra +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: "registry/basicauth-user" + desc: "Entry de pass con el usuario para basicAuth del registry API (linea 1)" + - name: "registry/basicauth-pass" + desc: "Entry de pass con la contraseña para basicAuth del registry API (linea 1)" + - name: "registry/api-token" + desc: "Entry de pass con el REGISTRY_API_TOKEN (linea 1)" + - name: "args" + desc: "Argumentos opcionales forwarded a fn sync: status, locations, o nada para push+pull completo" +output: "Mismo output que ./fn sync (stdin/stdout/stderr heredados). Exit code del subproceso fn sync." +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/pipelines/fn_sync_with_pass.sh" +--- + +## Ejemplo + +```bash +# Sync simple (push+pull completo) +./fn run fn_sync_with_pass_bash_pipelines + +# Ver estado local: PC, API, conteos +./fn run fn_sync_with_pass_bash_pipelines status + +# Mapa de ubicaciones cross-PC +./fn run fn_sync_with_pass_bash_pipelines locations +``` + +## Cuando usarla + +Cuando necesites ejecutar `fn sync` sin tener las credenciales exportadas en el entorno. Sustituye al bloque de `export FN_REGISTRY_API=...` que de otro modo habria que poner en `~/.zshrc`. + +## Gotchas + +- Si GPG no tiene la clave desbloqueada, `pass show` abre el prompt del agente gpg. Dejarlo pasar — no capturar stderr para no interferir con el pinentry. +- Requiere que el password-store este inicializado (`pass init`). Si no existe, `pass show` falla con error claro. +- `FN_REGISTRY_ROOT` debe apuntar a la raiz del registry donde vive el binario `./fn`. Si no esta seteado, se resuelve via `git rev-parse --show-toplevel`. +- Los tres entries de pass deben tener el valor en la **linea 1** (convencion estandar de pass). Metadata adicional en lineas siguientes es ignorada. diff --git a/bash/functions/pipelines/fn_sync_with_pass.sh b/bash/functions/pipelines/fn_sync_with_pass.sh new file mode 100644 index 00000000..eea0ace0 --- /dev/null +++ b/bash/functions/pipelines/fn_sync_with_pass.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# fn_sync_with_pass — Wrapper de fn sync que lee credenciales desde pass. +set -euo pipefail + +FN_ROOT="${FN_REGISTRY_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}" + +fn_sync_with_pass() { + command -v pass >/dev/null 2>&1 || { + echo "fn_sync_with_pass: 'pass' CLI no instalado. Instala con: apt install pass" >&2 + return 127 + } + + local u p t + + u=$(pass show registry/basicauth-user 2>/dev/null | head -n1) || { + echo "fn_sync_with_pass: falta registry/basicauth-user en pass. Crea con: pass insert registry/basicauth-user" >&2 + return 1 + } + p=$(pass show registry/basicauth-pass 2>/dev/null | head -n1) || { + echo "fn_sync_with_pass: falta registry/basicauth-pass en pass. Crea con: pass insert registry/basicauth-pass" >&2 + return 1 + } + t=$(pass show registry/api-token 2>/dev/null | head -n1) || { + echo "fn_sync_with_pass: falta registry/api-token en pass. Crea con: pass insert registry/api-token" >&2 + return 1 + } + + export FN_REGISTRY_API="https://${u}:${p}@registry.organic-machine.com" + export REGISTRY_API_TOKEN="$t" + + cd "$FN_ROOT" + ./fn sync "$@" +} + +# Ejecucion directa (no library mode) +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + fn_sync_with_pass "$@" +fi diff --git a/bash/functions/pipelines/init_cpp_app.sh b/bash/functions/pipelines/init_cpp_app.sh index 6d4fa7db..a49d979c 100755 --- a/bash/functions/pipelines/init_cpp_app.sh +++ b/bash/functions/pipelines/init_cpp_app.sh @@ -8,7 +8,7 @@ # Uso: # init_cpp_app [--project

] [--domain ] [--desc "..."] [--tags "a,b"] # -# Por defecto domain=tools, sin proyecto (cpp/apps//). +# Por defecto domain=tools, sin proyecto (apps//, issue 0096). set -euo pipefail @@ -55,7 +55,7 @@ init_cpp_app() { fi rel_dir="projects/$project/apps/$name" else - rel_dir="cpp/apps/$name" + rel_dir="apps/$name" fi abs_dir="$FN_ROOT/$rel_dir" @@ -201,11 +201,14 @@ if(EXISTS \${_${upper}_DIR}/CMakeLists.txt) endif() EOF else + local upper + upper="$(echo "$name" | tr '[:lower:]' '[:upper:]')" cat >> "$cpp_cmake" < void" +description: "Cross-compila TODOS los apps C++ del registry en un solo cmake pass y despliega cada .exe al Desktop de Windows. Mas rapido que N builds individuales. Acepta filtro de nombre para despliegue parcial." +tags: [cpp, windows, deploy, redeploy, bulk, cpp-windows] +uses_functions: + - build_cpp_windows_bash_infra + - deploy_cpp_exe_to_windows_bash_infra +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/pipelines/redeploy_all_cpp_apps.sh" +params: + - name: filter + desc: "Opcional. Substring para limitar el deploy a apps cuyo nombre lo contenga (ej: 'graph' solo despliega apps con 'graph' en el nombre). Sin valor = todas las apps." +output: "Imprime tabla resumen con OK/SKIPPED/FAILED y nombres de cada app. Exit 1 si al menos una app fallo el deploy." +--- + +## Ejemplo + +```bash +# Recompilar y redesplegar TODAS las apps C++ tras un cambio en cpp/framework/ +./fn run redeploy_all_cpp_apps + +# Solo apps cuyo nombre contenga "graph" +./fn run redeploy_all_cpp_apps graph +``` + +## Cuando usarla + +Tras un cambio en `cpp/framework/app_base.cpp`, `cpp/functions/core/*` o cualquier +funcion linkada a multiples apps. Ahorra correr `redeploy_cpp_app_windows

` +N veces — un solo cmake pass compila todo el arbol en paralelo. + +## Comportamiento + +1. **Build**: invoca `build_cpp_windows` sin argumento (compila todo el arbol con + `-j$(nproc)`). Un solo cmake pass — mucho mas rapido que N builds individuales. +2. **Descubrimiento**: itera `apps/*/CMakeLists.txt` y `projects/*/apps/*/CMakeLists.txt`. + **No** usa `cpp/apps/` (deprecado tras issue 0096). +3. **Filtro** (opcional): si se paso un argumento, solo procesa apps cuyo `basename` + contiene el substring. +4. **Por cada app**: + - Localiza `.exe` en `cpp/build/windows/apps//.exe`; si no existe, + busca bajo `cpp/build/windows/` como fallback. + - Si no hay `.exe`: log SKIP, continua (no aborta — apps headless o sub-repos no + clonados no tienen build target). + - `taskkill.exe /IM .exe /F` silencioso (no aborta si falla). + - `deploy_cpp_exe_to_windows ` (copia exe + DLLs + assets + + enrichers + runtime, preserva `local_files/`). + - Error por app: log FAILED, continua con la siguiente. +5. **Resumen final**: tabla `OK / SKIPPED / FAILED` con nombres. Exit 1 si hay + al menos un FAILED. + +## Variables de entorno + +| Variable | Default | Descripcion | +|---|---|---| +| `FN_REGISTRY_ROOT` | auto-detect | Raiz del registry (busca hacia arriba desde el script) | +| `BUILD_WIN` | `$root/cpp/build/windows` | Directorio de build Windows | +| `WIN_DESKTOP_APPS` | `/mnt/c/Users/lucas/Desktop/apps` | Destino de deploy en Windows | + +## Gotchas + +- Solo Windows (cross-compile mingw-w64 + Desktop deploy via WSL2). En Linux puro no aplica. +- `taskkill.exe` requiere WSL2 con interop habilitado. No funciona en WSL1 ni Linux nativo. +- Algunas apps pueden no estar en el grafo cmake actual (sub-repo no clonado, `add_subdirectory` + protegido por `if(EXISTS ...)`). El pipeline las SKIPea sin abortar — comportamiento esperado. +- Build paralelo puede consumir varios GB de RAM. Si hay OOM, reducir paralelismo exportando + `BUILD_JOBS=4` antes de invocar (actualmente la funcion `build_cpp_windows` usa `$(nproc)`; + si necesitas override edita `BUILD_JOBS` como variable de entorno custom o fork la funcion). +- El loop de deploy atrapa errores por app (`|| { failed+=...; continue; }`) para no abortar + en el primer fallo — todas las apps se intentan aunque alguna falle. + +## Capability growth log + +- v1.0.0 (2026-05-16) — creacion. Tras issue 0096 (apps movidas a `apps//`) el patron "recompilar+desplegar todas tras un cambio en `cpp/framework/`" se repitio varias veces sin un wrapper. Pipeline tolerante a fallos: build best-effort (test_* roto en mingw no aborta), deploy por app captura fallos individuales, summary OK/SKIPPED/FAILED al final. Primera corrida real (16 May 2026): 12 OK / 1 SKIP (`data_factory` sin .exe target) / 0 FAILED. + +## Notas operativas (2026-05-16) + +- `build_cpp_windows` sin arg compila el arbol entero. Si hay targets rotos (ej. `test_llm_anthropic`, `test_graph_icons` usan `setenv()` no disponible en mingw-w64), el pipeline logea `[1/2] Build returned exit=N — continuing with deploy of available exes` y sigue con la fase de deploy. Cada app sin `.exe` queda SKIPPED. +- Tras una corrida exitosa, los `.exe` quedan en `/mnt/c/Users/lucas/Desktop/apps//.exe`. Lanzar individualmente con `./fn run is_cpp_app_running_windows ` para chequear y `launch_cpp_app_windows ` para arrancar. diff --git a/bash/functions/pipelines/redeploy_all_cpp_apps.sh b/bash/functions/pipelines/redeploy_all_cpp_apps.sh new file mode 100644 index 00000000..ae5c1791 --- /dev/null +++ b/bash/functions/pipelines/redeploy_all_cpp_apps.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +# redeploy_all_cpp_apps — Cross-compila TODOS los apps C++ del registry en un solo +# cmake pass y despliega cada .exe al Desktop de Windows. +# Uso: redeploy_all_cpp_apps [filter] +# filter substring opcional para limitar el deploy a apps cuyo nombre lo contenga +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../infra/build_cpp_windows.sh" +source "$SCRIPT_DIR/../infra/deploy_cpp_exe_to_windows.sh" + +redeploy_all_cpp_apps() { + local filter="${1:-}" + + # --- Localizar raiz del registry --- + local root="${FN_REGISTRY_ROOT:-}" + if [ -z "$root" ]; then + local d="$SCRIPT_DIR" + while [ "$d" != "/" ]; do + if [ -f "$d/registry.db" ] && [ -d "$d/cpp" ]; then + root="$d"; break + fi + d="$(dirname "$d")" + done + fi + if [ -z "$root" ]; then + echo "[redeploy_all_cpp_apps] ERROR: no se localiza la raiz del registry. Exporta FN_REGISTRY_ROOT." >&2 + return 2 + fi + + local build_win="${BUILD_WIN:-$root/cpp/build/windows}" + + # --- Paso 1: compilar TODO el arbol (un solo cmake pass) --- + # Tolerante a fallos: si algun target (ej. test_* roto en mingw, app con + # bug puntual) falla, los demas exes que SI se construyeron siguen siendo + # desplegables. El loop de deploy hace SKIP por cada app sin .exe, asi que + # el modo "build best-effort + deploy lo que haya" es seguro. + echo "[1/2] Cross-compiling all C++ targets (best-effort)..." + local build_rc=0 + build_cpp_windows || build_rc=$? + if [ "$build_rc" -ne 0 ]; then + echo "[1/2] Build returned exit=$build_rc — continuing with deploy of available exes" >&2 + else + echo "[1/2] Build OK" + fi + + # --- Descubrir apps con CMakeLists.txt --- + # Busca en apps/*/ y projects/*/apps/*/ (no en cpp/apps/ — deprecado) + local -a app_dirs=() + while IFS= read -r cmakelists; do + app_dirs+=("$(dirname "$cmakelists")") + done < <( + find "$root/apps" -maxdepth 2 -name "CMakeLists.txt" 2>/dev/null | sort + find "$root/projects" -maxdepth 4 -path "*/apps/*/CMakeLists.txt" 2>/dev/null | sort + ) + + if [ ${#app_dirs[@]} -eq 0 ]; then + echo "[redeploy_all_cpp_apps] WARN: no se encontraron apps con CMakeLists.txt" >&2 + return 0 + fi + + # --- Paso 2: deploy por app --- + echo "[2/2] Deploying apps to Windows Desktop..." + local -a ok=() skipped=() failed=() + + for app_dir in "${app_dirs[@]}"; do + local name + name="$(basename "$app_dir")" + + # Aplicar filtro si se indico + if [ -n "$filter" ] && [[ "$name" != *"$filter"* ]]; then + continue + fi + + # Localizar el .exe en la ubicacion canonica + local exe_path="$build_win/apps/$name/$name.exe" + if [ ! -f "$exe_path" ]; then + # Fallback: buscar bajo build_win/ + exe_path="$(find "$build_win" -name "$name.exe" -type f 2>/dev/null | head -n1 || true)" + fi + + if [ -z "$exe_path" ] || [ ! -f "$exe_path" ]; then + echo " SKIP: $name — .exe no encontrado en $build_win" >&2 + skipped+=("$name") + continue + fi + + # taskkill silencioso (pre-autorizado; deploy_cpp_exe_to_windows lo hace internamente, + # pero si deploy falla antes de llegar ahi nos aseguramos de liberar el lock) + if command -v taskkill.exe >/dev/null 2>&1; then + taskkill.exe /IM "${name}.exe" /F >/dev/null 2>&1 || true + fi + + if deploy_cpp_exe_to_windows "$name" "$app_dir"; then + ok+=("$name") + else + echo " FAILED: $name" >&2 + failed+=("$name") + fi + done + + # --- Resumen --- + echo "" + echo "===== redeploy_all_cpp_apps — summary =====" + printf " OK : %d\n" "${#ok[@]}" + printf " SKIPPED : %d\n" "${#skipped[@]}" + printf " FAILED : %d\n" "${#failed[@]}" + + if [ ${#ok[@]} -gt 0 ]; then + echo " Deployed:" + for n in "${ok[@]}"; do printf " + %s\n" "$n"; done + fi + if [ ${#skipped[@]} -gt 0 ]; then + echo " Skipped (no .exe):" + for n in "${skipped[@]}"; do printf " - %s\n" "$n"; done + fi + if [ ${#failed[@]} -gt 0 ]; then + echo " Failed:" + for n in "${failed[@]}"; do printf " x %s\n" "$n"; done + return 1 + fi +} + +# Ejecutar si se llama directamente (fn run lo invoca como script) +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + redeploy_all_cpp_apps "$@" +fi diff --git a/cmd/fn/doctor.go b/cmd/fn/doctor.go index 72efc756..438cd1e3 100644 --- a/cmd/fn/doctor.go +++ b/cmd/fn/doctor.go @@ -59,6 +59,8 @@ func cmdDoctor(args []string) { } else { doctorCapabilities(r, jsonOut) } + case "app-location": + doctorAppLocation(r, jsonOut) default: fmt.Fprintf(os.Stderr, "unknown doctor subcommand: %s\n", sub) doctorUsage() @@ -84,6 +86,7 @@ Subcommands: vaults Salud de vaults: directorio, layout, índice, staleness, drift copied-code Detecta cuerpos de funcion del registry copiados en apps sin import (issue 0085k) capabilities Drift entre docs/capabilities/INDEX.md, tags de funciones, y paginas .md (issue 0086) + app-location Detecta artefactos (apps/analysis) en carpetas de lenguaje (cpp/apps/, etc.) - issue 0096 Flags: --json Salida JSON (para scripting/agentes) @@ -513,3 +516,26 @@ func doctorCopiedCode(root string, jsonOut bool) { w.Flush() fmt.Printf("\n%d suspected copy match(es).\n", len(entries)) } + +func doctorAppLocation(root string, jsonOut bool) { + violations, err := infra.AuditAppLocation(root) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + if jsonOut { + emit(violations) + return + } + if len(violations) == 0 { + fmt.Println("OK: no artefacts under language-named folders.") + return + } + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "KIND\tLANG\tPATH") + for _, v := range violations { + fmt.Fprintf(w, "%s\t%s\t%s\n", v.Kind, v.Lang, v.Path) + } + w.Flush() + fmt.Printf("\n%d violation(s): move artefact to apps// or projects/

/apps// (issue 0096).\n", len(violations)) +} diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 1f8fce62..24fd2564 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -1,5 +1,9 @@ cmake_minimum_required(VERSION 3.16) -project(fn_registry_cpp LANGUAGES C CXX) +if(WIN32) + project(fn_registry_cpp LANGUAGES C CXX RC) +else() + project(fn_registry_cpp LANGUAGES C CXX) +endif() set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -236,7 +240,18 @@ endif() set(FN_CPP_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE INTERNAL "fn_registry cpp root") function(add_imgui_app target) - add_executable(${target} ${ARGN}) + # Windows icon: si la app tiene /appicon.ico, generamos un .rc + # apuntando a ese .ico y lo anadimos como fuente. mingw-w64 windres + # (CMAKE_RC_COMPILER en la toolchain) lo enlaza en el .exe. + set(_extra_sources "") + if(WIN32 AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/appicon.ico) + set(_rc_file ${CMAKE_CURRENT_BINARY_DIR}/${target}_appicon.rc) + # Forward slashes para que windres no se confunda con escapes. + file(TO_CMAKE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/appicon.ico _ico_path) + file(WRITE ${_rc_file} "IDI_ICON1 ICON \"${_ico_path}\"\n") + list(APPEND _extra_sources ${_rc_file}) + endif() + add_executable(${target} ${ARGN} ${_extra_sources}) target_link_libraries(${target} PRIVATE fn_framework) target_include_directories(${target} PRIVATE ${FN_CPP_ROOT_DIR}/functions @@ -314,14 +329,20 @@ target_link_libraries(fn_table_viz PUBLIC target_link_libraries(fn_table_viz PRIVATE fn_framework) endif() -# --- Demo app --- -if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/chart_demo/CMakeLists.txt) - add_subdirectory(apps/chart_demo) +# --- Demo app (lives in apps/, issue 0096 standardization) --- +if(NOT DEFINED _CHART_DEMO_DIR) + set(_CHART_DEMO_DIR ${CMAKE_SOURCE_DIR}/../apps/chart_demo) +endif() +if(EXISTS ${_CHART_DEMO_DIR}/CMakeLists.txt) + add_subdirectory(${_CHART_DEMO_DIR} ${CMAKE_BINARY_DIR}/apps/chart_demo) endif() -# --- Shaders Lab --- -if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/shaders_lab/CMakeLists.txt) - add_subdirectory(apps/shaders_lab) +# --- Shaders Lab (lives in apps/) --- +if(NOT DEFINED _SHADERS_LAB_DIR) + set(_SHADERS_LAB_DIR ${CMAKE_SOURCE_DIR}/../apps/shaders_lab) +endif() +if(EXISTS ${_SHADERS_LAB_DIR}/CMakeLists.txt) + add_subdirectory(${_SHADERS_LAB_DIR} ${CMAKE_BINARY_DIR}/apps/shaders_lab) endif() # --- Lua 5.4 vendored (para playground tables / DSL formulas) --- @@ -329,30 +350,33 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/lua/CMakeLists.txt) add_subdirectory(vendor/lua) endif() -# --- Primitives Gallery (catalogo visual de primitivos core/viz/gfx) --- -if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/primitives_gallery/CMakeLists.txt) - add_subdirectory(apps/primitives_gallery) +# --- Primitives Gallery (lives in apps/) --- +if(NOT DEFINED _PG_DIR) + set(_PG_DIR ${CMAKE_SOURCE_DIR}/../apps/primitives_gallery) +endif() +if(EXISTS ${_PG_DIR}/CMakeLists.txt) + add_subdirectory(${_PG_DIR} ${CMAKE_BINARY_DIR}/apps/primitives_gallery) endif() # --- Tables playground (vive dentro de primitives_gallery/playground/tables/) --- -# No es un app del registry; sirve para iterar mejoras sobre table_view_cpp_viz -# antes de promover una API v2 y migrar las apps C++ que hoy usan ImGui::BeginTable raw. -if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/primitives_gallery/playground/tables/CMakeLists.txt) - add_subdirectory(apps/primitives_gallery/playground/tables) +if(EXISTS ${_PG_DIR}/playground/tables/CMakeLists.txt) + add_subdirectory(${_PG_DIR}/playground/tables ${CMAKE_BINARY_DIR}/apps/primitives_gallery/playground/tables) endif() -# --- text_editor + file_watcher smoke test (issue 0025) --- -# Build gate para validar que text_editor.cpp + file_watcher.cpp + vendor enlazan. -if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/text_editor_smoke/CMakeLists.txt) - add_subdirectory(apps/text_editor_smoke) +# --- text_editor + file_watcher smoke test (lives in apps/) --- +if(NOT DEFINED _TES_DIR) + set(_TES_DIR ${CMAKE_SOURCE_DIR}/../apps/text_editor_smoke) +endif() +if(EXISTS ${_TES_DIR}/CMakeLists.txt) + add_subdirectory(${_TES_DIR} ${CMAKE_BINARY_DIR}/apps/text_editor_smoke) endif() -# --- AltSnap viewport-jitter regression test --- -# Headless harness que conduce glfwSetWindowPos cada frame y verifica que -# ImGui viewport->Pos sigue al OS dentro de 1px. Sin la patch del framework -# (callback GLFW + per-frame sync) este test falla exit=1. -if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/altsnap_jitter_test/CMakeLists.txt) - add_subdirectory(apps/altsnap_jitter_test) +# --- AltSnap viewport-jitter regression test (lives in apps/) --- +if(NOT DEFINED _AJT_DIR) + set(_AJT_DIR ${CMAKE_SOURCE_DIR}/../apps/altsnap_jitter_test) +endif() +if(EXISTS ${_AJT_DIR}/CMakeLists.txt) + add_subdirectory(${_AJT_DIR} ${CMAKE_BINARY_DIR}/apps/altsnap_jitter_test) endif() # --- gamedev stack (SDL3 + sokol_gfx + miniaudio, issue 0072) --- @@ -368,11 +392,17 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sdl3/CMakeLists.txt set(SDL_INSTALL OFF CACHE BOOL "" FORCE) set(SDL_X11_XSCRNSAVER OFF CACHE BOOL "" FORCE) add_subdirectory(vendor/sdl3 EXCLUDE_FROM_ALL) - if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/engine_smoke/CMakeLists.txt) - add_subdirectory(apps/engine_smoke) + if(NOT DEFINED _ES_DIR) + set(_ES_DIR ${CMAKE_SOURCE_DIR}/../apps/engine_smoke) endif() - if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/runtime_test/CMakeLists.txt) - add_subdirectory(apps/runtime_test) + if(EXISTS ${_ES_DIR}/CMakeLists.txt) + add_subdirectory(${_ES_DIR} ${CMAKE_BINARY_DIR}/apps/engine_smoke) + endif() + if(NOT DEFINED _RT_DIR) + set(_RT_DIR ${CMAKE_SOURCE_DIR}/../apps/runtime_test) + endif() + if(EXISTS ${_RT_DIR}/CMakeLists.txt) + add_subdirectory(${_RT_DIR} ${CMAKE_BINARY_DIR}/apps/runtime_test) endif() endif() @@ -421,7 +451,16 @@ if(BUILD_TESTING) endif() -# --- dag_engine_ui --- -if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/dag_engine_ui/CMakeLists.txt) - add_subdirectory(apps/dag_engine_ui) +# --- dag_engine_ui (lives in apps/, issue 0096) --- +if(NOT DEFINED _DAG_UI_DIR) + set(_DAG_UI_DIR ${CMAKE_SOURCE_DIR}/../apps/dag_engine_ui) +endif() +if(EXISTS ${_DAG_UI_DIR}/CMakeLists.txt) + add_subdirectory(${_DAG_UI_DIR} ${CMAKE_BINARY_DIR}/apps/dag_engine_ui) +endif() + +# --- data_factory (lives in apps/, issue 0096) --- +set(_DATA_FACTORY_DIR ${CMAKE_SOURCE_DIR}/../apps/data_factory) +if(EXISTS ${_DATA_FACTORY_DIR}/CMakeLists.txt) + add_subdirectory(${_DATA_FACTORY_DIR} ${CMAKE_BINARY_DIR}/apps/data_factory) endif() diff --git a/cpp/apps/altsnap_jitter_test b/cpp/apps/altsnap_jitter_test deleted file mode 160000 index 6e52b658..00000000 --- a/cpp/apps/altsnap_jitter_test +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6e52b658a3fa089ad63d8c6b5d406e49301cc08b diff --git a/cpp/apps/chart_demo/CMakeLists.txt b/cpp/apps/chart_demo/CMakeLists.txt deleted file mode 100644 index 89ac54fe..00000000 --- a/cpp/apps/chart_demo/CMakeLists.txt +++ /dev/null @@ -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() diff --git a/cpp/apps/chart_demo/app.md b/cpp/apps/chart_demo/app.md deleted file mode 100644 index e2cf01d2..00000000 --- a/cpp/apps/chart_demo/app.md +++ /dev/null @@ -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. diff --git a/cpp/apps/chart_demo/main.cpp b/cpp/apps/chart_demo/main.cpp deleted file mode 100644 index 0e34b5fc..00000000 --- a/cpp/apps/chart_demo/main.cpp +++ /dev/null @@ -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 -#include - -// 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(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(rand()) / RAND_MAX * 10.0f; - scatter_y[i] = scatter_x[i] * 0.5f + (static_cast(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(fn_log::Level::Debug)} - }, render); -} -#endif diff --git a/cpp/apps/chart_demo/tests/chart_demo_tests.cpp b/cpp/apps/chart_demo/tests/chart_demo_tests.cpp deleted file mode 100644 index 51afd1fb..00000000 --- a/cpp/apps/chart_demo/tests/chart_demo_tests.cpp +++ /dev/null @@ -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); -} diff --git a/cpp/apps/dag_engine_ui b/cpp/apps/dag_engine_ui deleted file mode 160000 index aec22ba5..00000000 --- a/cpp/apps/dag_engine_ui +++ /dev/null @@ -1 +0,0 @@ -Subproject commit aec22ba5940e8560fd58e508a67374c1852774ad diff --git a/cpp/apps/engine_smoke b/cpp/apps/engine_smoke deleted file mode 160000 index bed33856..00000000 --- a/cpp/apps/engine_smoke +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bed33856e7e64a16344573a07a2a68fac3b4aa4d diff --git a/cpp/apps/primitives_gallery/CMakeLists.txt b/cpp/apps/primitives_gallery/CMakeLists.txt deleted file mode 100644 index c71a2139..00000000 --- a/cpp/apps/primitives_gallery/CMakeLists.txt +++ /dev/null @@ -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() diff --git a/cpp/apps/primitives_gallery/README.md b/cpp/apps/primitives_gallery/README.md deleted file mode 100644 index 59e6fec4..00000000 --- a/cpp/apps/primitives_gallery/README.md +++ /dev/null @@ -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. diff --git a/cpp/apps/primitives_gallery/app.md b/cpp/apps/primitives_gallery/app.md deleted file mode 100644 index aca75af5..00000000 --- a/cpp/apps/primitives_gallery/app.md +++ /dev/null @@ -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 -``` - -Renderiza cada demo offscreen y guarda PNGs en `/`. 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). diff --git a/cpp/apps/primitives_gallery/assets/sample.png b/cpp/apps/primitives_gallery/assets/sample.png deleted file mode 100644 index 5156f39c..00000000 Binary files a/cpp/apps/primitives_gallery/assets/sample.png and /dev/null differ diff --git a/cpp/apps/primitives_gallery/capture.cpp b/cpp/apps/primitives_gallery/capture.cpp deleted file mode 100644 index bc68f376..00000000 --- a/cpp/apps/primitives_gallery/capture.cpp +++ /dev/null @@ -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 - -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include "stb_image_write.h" - -#include -#include - -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 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& 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 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 diff --git a/cpp/apps/primitives_gallery/capture.h b/cpp/apps/primitives_gallery/capture.h deleted file mode 100644 index 10f5bc65..00000000 --- a/cpp/apps/primitives_gallery/capture.h +++ /dev/null @@ -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/.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 -#include - -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& items); - -} // namespace gallery diff --git a/cpp/apps/primitives_gallery/demo.cpp b/cpp/apps/primitives_gallery/demo.cpp deleted file mode 100644 index d2ab17a0..00000000 --- a/cpp/apps/primitives_gallery/demo.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include "demo.h" -#include "core/tokens.h" -#include - -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 diff --git a/cpp/apps/primitives_gallery/demo.h b/cpp/apps/primitives_gallery/demo.h deleted file mode 100644 index 94177743..00000000 --- a/cpp/apps/primitives_gallery/demo.h +++ /dev/null @@ -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 - -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 diff --git a/cpp/apps/primitives_gallery/demos.h b/cpp/apps/primitives_gallery/demos.h deleted file mode 100644 index 8e460f3c..00000000 --- a/cpp/apps/primitives_gallery/demos.h +++ /dev/null @@ -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 diff --git a/cpp/apps/primitives_gallery/demos_3d.cpp b/cpp/apps/primitives_gallery/demos_3d.cpp deleted file mode 100644 index 30f8c505..00000000 --- a/cpp/apps/primitives_gallery/demos_3d.cpp +++ /dev/null @@ -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 -#include -#include -#include - -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", &, 0.1f, 3.0f, "%.2f"); - - constexpr int N = 64; - static std::vector 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 xs(N), ys(N), zs(N); - static std::vector colors(N); - static bool initialized = false; - - if (!initialized) { - std::mt19937 rng(42); - std::normal_distribution 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 diff --git a/cpp/apps/primitives_gallery/demos_animation.cpp b/cpp/apps/primitives_gallery/demos_animation.cpp deleted file mode 100644 index 877324a0..00000000 --- a/cpp/apps/primitives_gallery/demos_animation.cpp +++ /dev/null @@ -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 - -#include -#include - -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 diff --git a/cpp/apps/primitives_gallery/demos_core.cpp b/cpp/apps/primitives_gallery/demos_core.cpp deleted file mode 100644 index 3d2566bd..00000000 --- a/cpp/apps/primitives_gallery/demos_core.cpp +++ /dev/null @@ -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 -#include - -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 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 diff --git a/cpp/apps/primitives_gallery/demos_extras.cpp b/cpp/apps/primitives_gallery/demos_extras.cpp deleted file mode 100644 index dec8ace3..00000000 --- a/cpp/apps/primitives_gallery/demos_extras.cpp +++ /dev/null @@ -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 -#include -#include -#include -#include -#include - -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 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 diff --git a/cpp/apps/primitives_gallery/demos_gfx.cpp b/cpp/apps/primitives_gallery/demos_gfx.cpp deleted file mode 100644 index b69e50a9..00000000 --- a/cpp/apps/primitives_gallery/demos_gfx.cpp +++ /dev/null @@ -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 -#include - -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( - 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(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 diff --git a/cpp/apps/primitives_gallery/demos_gl_texture.cpp b/cpp/apps/primitives_gallery/demos_gl_texture.cpp deleted file mode 100644 index 0d7e05c5..00000000 --- a/cpp/apps/primitives_gallery/demos_gl_texture.cpp +++ /dev/null @@ -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 -#include -#include - -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 diff --git a/cpp/apps/primitives_gallery/demos_graph.cpp b/cpp/apps/primitives_gallery/demos_graph.cpp deleted file mode 100644 index 11cce7ab..00000000 --- a/cpp/apps/primitives_gallery/demos_graph.cpp +++ /dev/null @@ -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 -#include -#include -#include - -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& nodes_out, - std::vector& 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((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(std::max(N, 1))); - const float cluster_r = 12.0f * scale; - const float scatter = 4.0f * scale; - - std::vector 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(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(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(rnd() * size); - add_edge(static_cast(i), - static_cast(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(rnd() * N); - uint32_t b = static_cast(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 s_nodes; - static std::vector 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(s_nodes.size()); - s_graph.node_capacity = static_cast(s_nodes.capacity()); - s_graph.edges = s_edges.data(); - s_graph.edge_count = static_cast(s_edges.size()); - s_graph.edge_capacity = static_cast(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 "#" 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(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 diff --git a/cpp/apps/primitives_gallery/demos_graph_styles.cpp b/cpp/apps/primitives_gallery/demos_graph_styles.cpp deleted file mode 100644 index 917f8255..00000000 --- a/cpp/apps/primitives_gallery/demos_graph_styles.cpp +++ /dev/null @@ -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 -#include -#include -#include - -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& nodes, - std::vector& 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 s_nodes; - static std::vector 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 diff --git a/cpp/apps/primitives_gallery/demos_mesh.cpp b/cpp/apps/primitives_gallery/demos_mesh.cpp deleted file mode 100644 index 5626f35e..00000000 --- a/cpp/apps/primitives_gallery/demos_mesh.cpp +++ /dev/null @@ -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 -#include -#include - -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 diff --git a/cpp/apps/primitives_gallery/demos_scientific.cpp b/cpp/apps/primitives_gallery/demos_scientific.cpp deleted file mode 100644 index 632b6c9a..00000000 --- a/cpp/apps/primitives_gallery/demos_scientific.cpp +++ /dev/null @@ -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 -#include -#include -#include - -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 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 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 nodes = { - {"premium"}, {"basicos"}, - {"laptops"}, {"phones"}, {"tablets"}, - {"hardware"}, {"software"}, {"servicios"}, - }; - std::vector 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 nodes = {{\"premium\"}, {\"basicos\"}, ...};\n" - "std::vector 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 diff --git a/cpp/apps/primitives_gallery/demos_sql.cpp b/cpp/apps/primitives_gallery/demos_sql.cpp deleted file mode 100644 index 01dc4a3e..00000000 --- a/cpp/apps/primitives_gallery/demos_sql.cpp +++ /dev/null @@ -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 -#include - -#include -#include -#include -#include - -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 diff --git a/cpp/apps/primitives_gallery/demos_text_editor.cpp b/cpp/apps/primitives_gallery/demos_text_editor.cpp deleted file mode 100644 index c6a32f4f..00000000 --- a/cpp/apps/primitives_gallery/demos_text_editor.cpp +++ /dev/null @@ -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 - -#include -#include -#include -#include -#include -#include - -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 \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 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 diff --git a/cpp/apps/primitives_gallery/demos_viz.cpp b/cpp/apps/primitives_gallery/demos_viz.cpp deleted file mode 100644 index a5b81117..00000000 --- a/cpp/apps/primitives_gallery/demos_viz.cpp +++ /dev/null @@ -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 -#include -#include - -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(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((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((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 diff --git a/cpp/apps/primitives_gallery/main.cpp b/cpp/apps/primitives_gallery/main.cpp deleted file mode 100644 index 22c76f6d..00000000 --- a/cpp/apps/primitives_gallery/main.cpp +++ /dev/null @@ -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 -#include -#include -#include -#include -#include - -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 ` 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 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 - ); -} diff --git a/cpp/apps/primitives_gallery/playground/tables/CMakeLists.txt b/cpp/apps/primitives_gallery/playground/tables/CMakeLists.txt deleted file mode 100644 index 7b2a883b..00000000 --- a/cpp/apps/primitives_gallery/playground/tables/CMakeLists.txt +++ /dev/null @@ -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() diff --git a/cpp/apps/primitives_gallery/playground/tables/data_table.cpp b/cpp/apps/primitives_gallery/playground/tables/data_table.cpp deleted file mode 100644 index d3bc059b..00000000 --- a/cpp/apps/primitives_gallery/playground/tables/data_table.cpp +++ /dev/null @@ -1,3393 +0,0 @@ -#include "data_table.h" -#include "app_base.h" -#include "imgui.h" -#include "llm_anthropic.h" -#include "lua_engine.h" -#include "tql.h" -#include "tql_to_sql.h" -#ifdef FN_TQL_DUCKDB -# include "tql_duckdb.h" -#endif -#include "viz.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace data_table { - -// UTC date today as ISO YYYY-MM-DD. Para preset filtros Last7/30/90d. -static std::string today_iso() { - std::time_t t = std::time(nullptr); - std::tm tm = *std::gmtime(&t); - char buf[16]; - std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d", - tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday); - return buf; -} - -namespace { - -// --------------------------------------------------------------------------- -// UI state global por-instancia (singleton playground). -// --------------------------------------------------------------------------- -struct UiState { - int pending_col = -1; - std::string pending_value; - bool open_cell_popup = false; - - int header_popup_col = -1; - std::unordered_map filter_inputs; - std::unordered_map color_value_inputs; - std::unordered_map color_picker_vals; - - int addf_col = 0; - std::string addf_val; - bool addf_range = false; - std::string addf_lo; - std::string addf_hi; - - int sel_anchor_row = -1; - int sel_anchor_col = -1; - int sel_end_row = -1; - int sel_end_col = -1; - bool sel_active = false; - bool sel_dragging = false; - - std::string last_export_path; - - // Modal de columna custom (formula). - bool cf_open = false; - bool cf_editing = false; - int cf_edit_idx = -1; - int cf_target_stage = 0; // stage donde se guarda la formula - std::string cf_formula; - std::string cf_name; - ColumnType cf_type = ColumnType::String; - std::string cf_error; - - bool cf_ac_open = false; - int cf_ac_start = -1; - int cf_ac_cursor = -1; - std::string cf_ac_filter; - bool cf_force_cursor = false; - int cf_target_cursor = -1; - - // TQL modales. - bool tql_show_open = false; - std::string tql_show_text; - bool tql_apply_open = false; - std::string tql_apply_text; - std::string tql_apply_error; - char tql_file_path[256] = "table.tql"; - std::string tql_io_status; // mensaje "saved" / "loaded" / error - - // Add-breakout popup (stage > 0). - int brk_picker_col = 0; - - // Add-aggregation popup (stage > 0). - int agg_picker_fn = (int)AggFn::Count; - int agg_picker_col = 0; - double agg_picker_arg = 0.95; - - // Edit chip popups: click der sobre chip. - // 0=none, 1=filter, 2=breakout, 3=agg, 4=sort - int edit_chip_kind = 0; - int edit_chip_idx = -1; - int edit_col_idx = 0; // combo idx para col picker - int edit_op = (int)Op::Eq; - int edit_agg_fn = (int)AggFn::Count; - double edit_agg_arg = 0.5; - bool edit_sort_desc = false; - std::string edit_value; - - // Add-sort popup (any stage). - int sort_picker_col = 0; - bool sort_picker_desc = false; - - bool stats_mode = false; - std::vector stats_cache; - const char* const* last_cells = nullptr; - int last_rows = -1; - int last_eff_cols = -1; - size_t last_filter_h = (size_t)-1; - int last_visible = -1; - - // Snapshot del active stage output para el config popup. - std::vector active_headers; - std::vector active_types; - // Snapshot del INPUT del active stage (= output del previo o orig+derived - // si active==0). Para que el config popup pueda cambiar la categoria del - // breakout del stage activo eligiendo de las cols upstream. - std::vector input_headers_active; - std::vector input_types_active; - - // Para forzar re-fit en cambio de display/stage/config. - ViewMode prev_viz_display = ViewMode::Table; - int prev_viz_stage = 0; - size_t prev_viz_cfg_h = 0; - - // show_chrome user override. - bool chrome_user_set = false; - bool chrome_user_visible = true; - - // Toggle Table <-> View: remember last non-table display. - ViewMode last_non_table_main = ViewMode::Bar; - - // Drill history (fase 10). Stacks per-app; no persistido en TQL. - std::vector drill_back; - std::vector drill_forward; - - // Row inspector (fase 10). -1 cerrado, sino row idx en el output del stage activo. - int inspect_row = -1; - bool inspect_open = false; - - // Ask AI modal (fase 11 — issue 0080). - bool ask_open = false; - bool ask_busy = false; - int ask_mode = 0; // 0 = TQL, 1 = SQL - char ask_question[2048] = {0}; - std::string ask_current_tql; // emit del state actual al abrir modal - std::string ask_response_raw; // texto del modelo - std::string ask_response_code; // bloque extraido (Lua o SQL) - std::string ask_error; - std::string ask_status; // "Sent. Waiting..." / "OK" / error - char ask_edit_buf[8192] = {0}; // buffer editable de propuesta -}; - -UiState& ui() { static UiState s; return s; } - -// Row inspector modal (fase 10). Muestra todas cols + valores de la fila -// inspect_row del output del stage activo. Read-only + Copy TSV + Filter -// by this row (anade filters al stage previo si existe). -static void draw_row_inspector_modal(State& st, int active, - const char* const* cells, int rows, int cols, - const std::vector& headers, - const std::vector& types, - const std::vector& prev_input_headers) { - auto& U = ui(); - if (!U.inspect_open) return; - if (U.inspect_row < 0 || U.inspect_row >= rows) { - U.inspect_open = false; - return; - } - ImGui::OpenPopup("##row_inspector"); - ImGui::SetNextWindowSize(ImVec2(560, 400), ImGuiCond_Appearing); - if (ImGui::BeginPopupModal("##row_inspector", &U.inspect_open, - ImGuiWindowFlags_NoSavedSettings)) { - ImGui::Text("Row %d", U.inspect_row); - ImGui::SameLine(0, 20); - if (ImGui::SmallButton("Copy TSV")) { - std::string tsv = row_to_tsv(cells, rows, cols, U.inspect_row, headers); - ImGui::SetClipboardText(tsv.c_str()); - } - ImGui::SameLine(); - bool can_filter = (active > 0 && !prev_input_headers.empty()); - ImGui::BeginDisabled(!can_filter); - if (ImGui::SmallButton("Filter prev stage by this row")) { - int target = active - 1; - for (int c = 0; c < cols; ++c) { - const char* v = cells[U.inspect_row * cols + c]; - if (!v || !*v) continue; - const std::string& h = headers[c]; - std::string h_clean; - parse_breakout_granularity(h, h_clean); - int ci = -1; - for (size_t i = 0; i < prev_input_headers.size(); ++i) { - if (prev_input_headers[i] == h_clean) { ci = (int)i; break; } - } - if (ci < 0) continue; - DrillStep step; - step.target_stage = target; - step.filter_pos = (int)st.stages[target].filters.size(); - step.prev_active_stage = st.active_stage; - step.added = make_drill_filter(ci, v); - if (apply_drill_step(st, step)) { - U.drill_back.push_back(step); - } - } - U.drill_forward.clear(); - U.inspect_open = false; - } - ImGui::EndDisabled(); - ImGui::Separator(); - ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg - | ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable; - if (ImGui::BeginTable("##inspector_tbl", 2, flags, ImVec2(-1, -1))) { - ImGui::TableSetupColumn("col"); - ImGui::TableSetupColumn("value"); - ImGui::TableHeadersRow(); - for (int c = 0; c < cols; ++c) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ColumnType t = (c < (int)types.size()) ? types[c] : ColumnType::String; - ImGui::Text("%s %s", column_type_icon(t), - (c < (int)headers.size()) ? headers[c].c_str() : "?"); - ImGui::TableSetColumnIndex(1); - const char* v = cells[U.inspect_row * cols + c]; - ImGui::TextWrapped("%s", v ? v : ""); - } - ImGui::EndTable(); - } - ImGui::EndPopup(); - } -} - -int autocomplete_cb(ImGuiInputTextCallbackData* data) { - UiState* U = (UiState*)data->UserData; - if (data->EventFlag == ImGuiInputTextFlags_CallbackAlways) { - if (U->cf_force_cursor) { - data->CursorPos = U->cf_target_cursor; - U->cf_force_cursor = false; - } - } - if (data->EventFlag == ImGuiInputTextFlags_CallbackEdit) { - std::string filter; - int idx = find_open_bracket(data->Buf, data->BufTextLen, - data->CursorPos, filter); - if (idx >= 0) { - U->cf_ac_open = true; - U->cf_ac_start = idx; - U->cf_ac_cursor = data->CursorPos; - U->cf_ac_filter = filter; - } else { - U->cf_ac_open = false; - } - } - return 0; -} - -size_t filters_hash(const std::vector& f) { - size_t h = 0xcbf29ce484222325ULL; - for (const auto& x : f) { - h ^= (size_t)x.col; h *= 0x100000001b3ULL; - h ^= (size_t)x.op; h *= 0x100000001b3ULL; - for (char ch : x.value) { h ^= (size_t)(unsigned char)ch; h *= 0x100000001b3ULL; } - } - return h; -} - -void ensure_init(State& st, int eff_cols) { - if ((int)st.col_visible.size() < eff_cols) st.col_visible.resize(eff_cols, true); - if ((int)st.col_order.size() != eff_cols) { - std::vector next; - next.reserve(eff_cols); - for (int x : st.col_order) if (x >= 0 && x < eff_cols) next.push_back(x); - std::vector seen(eff_cols, false); - for (int x : next) seen[x] = true; - for (int i = 0; i < eff_cols; ++i) if (!seen[i]) next.push_back(i); - st.col_order = std::move(next); - } - if ((int)st.col_visible.size() < (int)st.col_order.size()) - st.col_visible.resize(st.col_order.size(), true); -} - -// --------------------------------------------------------------------------- -// Breadcrumb stages: fila de botones Raw > Stage 1 > Stage 2 ... [+ Stage] -// --------------------------------------------------------------------------- -void draw_stage_breadcrumb(State& st) { - st.ensure_stage0(); - - // Drill history back/forward (fase 10). Botones al inicio. - auto& U = ui(); - { - bool can_back = !U.drill_back.empty(); - ImGui::BeginDisabled(!can_back); - if (ImGui::SmallButton("<##drill_back")) { - DrillStep s = U.drill_back.back(); - U.drill_back.pop_back(); - if (undo_drill_step(st, s)) { - U.drill_forward.push_back(s); - } - } - ImGui::EndDisabled(); - if (can_back && ImGui::IsItemHovered()) - ImGui::SetTooltip("Drill back (%zu)", U.drill_back.size()); - ImGui::SameLine(); - bool can_fwd = !U.drill_forward.empty(); - ImGui::BeginDisabled(!can_fwd); - if (ImGui::SmallButton(">##drill_fwd")) { - DrillStep s = U.drill_forward.back(); - U.drill_forward.pop_back(); - if (apply_drill_step(st, s)) { - U.drill_back.push_back(s); - } - } - ImGui::EndDisabled(); - if (can_fwd && ImGui::IsItemHovered()) - ImGui::SetTooltip("Drill forward (%zu)", U.drill_forward.size()); - ImGui::SameLine(); - bool can_up = (st.active_stage > 0); - ImGui::BeginDisabled(!can_up); - if (ImGui::SmallButton("^##drill_up")) drill_up(st); - ImGui::EndDisabled(); - if (can_up && ImGui::IsItemHovered()) - ImGui::SetTooltip("Drill up (stage previo, sin perder filters)"); - ImGui::SameLine(); - ImGui::TextDisabled("|"); - ImGui::SameLine(); - } - - for (int si = 0; si < (int)st.stages.size(); ++si) { - if (si > 0) { ImGui::SameLine(); ImGui::TextDisabled(">"); ImGui::SameLine(); } - - bool active = (si == st.active_stage); - if (active) { - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 80, 140, 200, 240)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(100, 160, 220, 240)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 60, 120, 180, 240)); - } else { - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 70, 70, 90, 200)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 90, 90, 120, 220)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 55, 55, 75, 220)); - } - - char label[256]; - if (si == 0) { - std::snprintf(label, sizeof(label), "Raw##stage%d", si); - } else { - const Stage& s = st.stages[si]; - std::string desc; - for (size_t i = 0; i < s.breakouts.size() && i < 2; ++i) { - if (i > 0) desc += ", "; - desc += s.breakouts[i]; - } - if (s.breakouts.size() > 2) desc += "..."; - if (desc.empty()) - std::snprintf(label, sizeof(label), "Stage %d##s%d", si, si); - else - std::snprintf(label, sizeof(label), "Stage %d: by %s##s%d", - si, desc.c_str(), si); - } - if (ImGui::Button(label)) st.active_stage = si; - ImGui::PopStyleColor(3); - - if (si > 0) { - ImGui::SameLine(); - char xlbl[32]; - std::snprintf(xlbl, sizeof(xlbl), "x##rm_s%d", si); - if (ImGui::SmallButton(xlbl)) { - // borra ese stage y sucesores - while ((int)st.stages.size() > si) st.stages.pop_back(); - if (st.active_stage >= (int)st.stages.size()) - st.active_stage = (int)st.stages.size() - 1; - if (st.active_stage < 0) st.active_stage = 0; - break; - } - } - } - ImGui::SameLine(); - ImGui::TextDisabled(">"); - ImGui::SameLine(); - if (ImGui::SmallButton("+ Stage##add_stage")) { - st.stages.push_back(Stage{}); - st.active_stage = (int)st.stages.size() - 1; - } -} - -struct ColInfo { std::string name; ColumnType type; }; -std::vector collect_active_col_info(const State& st); - -// Auto-promote: si user en stage 0 elige una viz que necesita agrupacion, -// crea stage 1 con breakout=primera cat + agg=sum(primera num) o count. -void auto_promote_aggregated(State& st) { - auto& U = ui(); - if (st.active_stage != 0) return; - if (st.stages.size() != 1) return; - - std::string cat_name; - std::string num_name; - for (size_t i = 0; i < U.active_headers.size() && i < U.active_types.size(); ++i) { - ColumnType t = U.active_types[i]; - if (cat_name.empty() && - (t == ColumnType::String || t == ColumnType::Date || - t == ColumnType::Bool || t == ColumnType::Json)) { - cat_name = U.active_headers[i]; - } - if (num_name.empty() && - (t == ColumnType::Int || t == ColumnType::Float)) { - num_name = U.active_headers[i]; - } - } - - Stage s1; - if (!cat_name.empty()) s1.breakouts.push_back(cat_name); - Aggregation a; - if (!num_name.empty()) { - a.fn = AggFn::Sum; - a.col = num_name; - } else { - a.fn = AggFn::Count; - } - s1.aggregations.push_back(a); - st.stages.push_back(std::move(s1)); - st.active_stage = (int)st.stages.size() - 1; -} - -// Toggle simple: un solo boton que alterna entre Table y el last_non_table. -// Para el main pasa st (para poder auto-promote a stage agregado si la viz -// destino lo requiere). Para extras usar overload sin State. -void draw_table_toggle(ViewMode& display, ViewMode& last_non_table, - const char* id_suffix, State* st_opt = nullptr) { - bool is_table = (display == ViewMode::Table); - char b[64]; - std::snprintf(b, sizeof(b), "%s##tbl_%s", - is_table ? "Show chart" : "Show table", id_suffix); - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(80, 140, 200, 240)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(100, 160, 220, 240)); - if (ImGui::SmallButton(b)) { - if (is_table) { - ViewMode tgt = (last_non_table == ViewMode::Table) - ? ViewMode::Bar : last_non_table; - display = tgt; - if (st_opt && view_mode_needs_aggregation(tgt)) { - auto_promote_aggregated(*st_opt); - } - } else { - last_non_table = display; - display = ViewMode::Table; - } - } - ImGui::PopStyleColor(2); -} - -// Render extra viz panel: child window con toolbar mini + chart. -// Devuelve true si user pidio cerrar. -bool draw_extra_panel(State& st, VizPanel& p, int idx, const StageOutput& so) { - bool close_req = false; - char child_id[64]; std::snprintf(child_id, sizeof(child_id), "##extra_viz_%d", idx); - ImGui::BeginChild(child_id, ImVec2(0, 320), true); - - // Toolbar - int n_modes = 0; - const ViewMode* modes = all_view_modes(&n_modes); - ImGui::TextDisabled("View:"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(180); - char combo_id[64]; std::snprintf(combo_id, sizeof(combo_id), "##ev_mode_%d", idx); - if (ImGui::BeginCombo(combo_id, view_mode_label(p.display))) { - for (int i = 0; i < n_modes; ++i) { - bool sel = (modes[i] == p.display); - if (ImGui::Selectable(view_mode_label(modes[i]), sel)) { - p.display = modes[i]; - p.config.fit_request = true; - } - if (sel) ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - ImGui::SameLine(); - char fit_id[32]; std::snprintf(fit_id, sizeof(fit_id), "Fit##ev_fit_%d", idx); - if (ImGui::SmallButton(fit_id)) p.config.fit_request = true; - ImGui::SameLine(); - char lock_id[32]; std::snprintf(lock_id, sizeof(lock_id), "%s##ev_lock_%d", - p.config.locked ? "Locked" : "Lock", idx); - if (p.config.locked) { - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(180, 60, 60, 230)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(200, 80, 80, 240)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(150, 40, 40, 240)); - } - if (ImGui::SmallButton(lock_id)) p.config.locked = !p.config.locked; - if (p.config.locked) ImGui::PopStyleColor(3); - ImGui::SameLine(); - char close_id[32]; std::snprintf(close_id, sizeof(close_id), "X##ev_close_%d", idx); - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(120, 50, 50, 220)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(160, 70, 70, 240)); - if (ImGui::SmallButton(close_id)) close_req = true; - ImGui::PopStyleColor(2); - - // Toggle Table <-> View per-panel - char ts[32]; std::snprintf(ts, sizeof(ts), "ep%d", idx); - draw_table_toggle(p.display, p.last_non_table, ts); - - // Render: si Table -> mini table; else chart. - if (p.display == ViewMode::Table) { - ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | - ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY | - ImGuiTableFlags_ScrollX; - char tid[64]; std::snprintf(tid, sizeof(tid), "##ep_table_%d", idx); - if (so.cols > 0 && ImGui::BeginTable(tid, so.cols, flags, ImVec2(0, 0))) { - for (int c = 0; c < so.cols; ++c) - ImGui::TableSetupColumn(so.headers[c].c_str()); - ImGui::TableHeadersRow(); - for (int r = 0; r < so.rows; ++r) { - ImGui::TableNextRow(); - for (int c = 0; c < so.cols; ++c) { - ImGui::TableSetColumnIndex(c); - const char* s = so.cells[(size_t)r * so.cols + c]; - ImGui::TextUnformatted(s ? s : ""); - } - } - ImGui::EndTable(); - } - } else { - viz::render(so, p.display, p.config, ImVec2(-1, -1)); - } - - ImGui::EndChild(); - (void)st; - return close_req; -} - -void draw_viz_config_popup(State& st) { - if (!ImGui::BeginPopup("##viz_cfg_popup")) return; - ImGui::Text("Configure: %s", view_mode_label(st.display)); - ImGui::Separator(); - - auto cols = collect_active_col_info(st); - std::vector all_names; - std::vector num_names; - std::vector cat_names; - for (auto& c : cols) { - all_names.push_back(c.name.c_str()); - if (c.type == ColumnType::Int || c.type == ColumnType::Float) - num_names.push_back(c.name.c_str()); - else - cat_names.push_back(c.name.c_str()); - } - - auto& vc = st.viz_config; - ViewMode m = st.display; - - auto combo_for_col = [&](const char* label, std::string& target, - const std::vector& options) { - const char* preview = target.empty() ? "(auto)" : target.c_str(); - ImGui::SetNextItemWidth(220); - if (ImGui::BeginCombo(label, preview)) { - if (ImGui::Selectable("(auto)", target.empty())) target.clear(); - for (auto& o : options) { - bool sel = (target == o); - if (ImGui::Selectable(o, sel)) target = o; - } - ImGui::EndCombo(); - } - }; - - // X col: scatter, line, area, stairs, hist2d, bubble - bool needs_x = (m == ViewMode::Scatter || m == ViewMode::Line || - m == ViewMode::Area || m == ViewMode::Stairs || - m == ViewMode::Histogram2D || m == ViewMode::Bubble); - if (needs_x) combo_for_col("X column", vc.x_col, num_names); - - // Y cols: most modes - bool needs_y = (m != ViewMode::Pie && m != ViewMode::Donut && m != ViewMode::Funnel && - m != ViewMode::Candlestick); - if (needs_y) { - ImGui::Text("Y columns:"); - ImGui::SameLine(); - ImGui::TextDisabled("(%d selected; empty = auto)", (int)vc.y_cols.size()); - ImGui::Indent(); - // Show checkbox for each numeric col - for (auto& nn : num_names) { - std::string ns = nn; - bool checked = std::find(vc.y_cols.begin(), vc.y_cols.end(), ns) != vc.y_cols.end(); - if (ImGui::Checkbox(nn, &checked)) { - if (checked) vc.y_cols.push_back(ns); - else { - auto it = std::find(vc.y_cols.begin(), vc.y_cols.end(), ns); - if (it != vc.y_cols.end()) vc.y_cols.erase(it); - } - } - } - ImGui::Unindent(); - if (ImGui::SmallButton("Clear Y##clr_y")) vc.y_cols.clear(); - } - - // Cat col: bar/pie/funnel/box/waterfall - bool needs_cat = (m == ViewMode::Bar || m == ViewMode::Column || - m == ViewMode::GroupedBar || m == ViewMode::StackedBar || - m == ViewMode::Pie || m == ViewMode::Donut || - m == ViewMode::Funnel || m == ViewMode::BoxPlot || - m == ViewMode::Waterfall); - if (needs_cat) { - // Si el active stage YA esta agrupado (breakouts != empty), la categoria - // del chart la dicta el breakout. Mostrar todas las cols del INPUT del - // stage (= cols pre-agrupacion). Selecionar otra = reemplaza breakouts[0] - // (re-agrupa). - int as = st.active_stage; - bool grouped = (as >= 0 && as < (int)st.stages.size() && - !st.stages[as].breakouts.empty()); - const auto& U = ui(); - if (grouped) { - std::vector input_cat_names; - for (size_t i = 0; i < U.input_headers_active.size() && - i < U.input_types_active.size(); ++i) { - ColumnType t = U.input_types_active[i]; - if (t == ColumnType::String || t == ColumnType::Date || - t == ColumnType::Bool || t == ColumnType::Json) { - input_cat_names.push_back(U.input_headers_active[i].c_str()); - } - } - std::string cur_break = st.stages[as].breakouts[0]; - const char* preview = cur_break.empty() ? "(none)" : cur_break.c_str(); - ImGui::SetNextItemWidth(220); - if (ImGui::BeginCombo("Category (breakout)", preview)) { - for (auto& o : input_cat_names) { - bool sel = (cur_break == o); - if (ImGui::Selectable(o, sel)) { - st.stages[as].breakouts[0] = o; - } - } - ImGui::EndCombo(); - } - } else { - combo_for_col("Category", vc.cat_col, cat_names); - } - } - - // Size col: bubble - if (m == ViewMode::Bubble) combo_for_col("Size column", vc.size_col, num_names); - - // Color - ImGui::Separator(); - float col_f[4] = { - ((vc.primary_color) & 0xFF) / 255.0f, - ((vc.primary_color >> 8) & 0xFF) / 255.0f, - ((vc.primary_color >> 16) & 0xFF) / 255.0f, - ((vc.primary_color >> 24) & 0xFF) / 255.0f, - }; - if (vc.primary_color == 0) { col_f[0]=col_f[1]=col_f[2]=1.0f; col_f[3]=1.0f; } - if (ImGui::ColorEdit4("Primary color", col_f, ImGuiColorEditFlags_AlphaBar)) { - unsigned int r = (unsigned int)(col_f[0] * 255); - unsigned int g = (unsigned int)(col_f[1] * 255); - unsigned int b = (unsigned int)(col_f[2] * 255); - unsigned int a = (unsigned int)(col_f[3] * 255); - vc.primary_color = (a << 24) | (b << 16) | (g << 8) | r; - } - ImGui::SameLine(); - if (ImGui::SmallButton("Auto##color")) vc.primary_color = 0; - - // Hist bins - if (m == ViewMode::Histogram || m == ViewMode::Histogram2D) { - ImGui::SetNextItemWidth(120); - int b = vc.hist_bins; - if (ImGui::InputInt("Bins (0=auto)", &b)) { - if (b < 0) b = 0; - vc.hist_bins = b; - } - } - - // Pie radius - if (m == ViewMode::Pie || m == ViewMode::Donut) { - ImGui::SetNextItemWidth(120); - float r = vc.pie_radius; - if (ImGui::SliderFloat("Radius (0=auto)", &r, 0.0f, 0.5f, "%.2f")) { - vc.pie_radius = r; - } - } - - // Toggles - ImGui::Separator(); - ImGui::Checkbox("Show legend", &vc.show_legend); - if (m == ViewMode::Line || m == ViewMode::Area || m == ViewMode::Stairs) { - ImGui::SameLine(); - ImGui::Checkbox("Show markers", &vc.show_markers); - } - - ImGui::Separator(); - if (ImGui::SmallButton("Reset config")) { - vc = ViewConfig{}; - } - ImGui::SameLine(); - if (ImGui::SmallButton("Close")) ImGui::CloseCurrentPopup(); - - ImGui::EndPopup(); -} - -// Devuelve nombres + tipos del active stage output snapshot (poblado por render). -std::vector collect_active_col_info(const State& st) { - (void)st; - auto& U = ui(); - std::vector r; - int n = (int)std::min(U.active_headers.size(), U.active_types.size()); - r.reserve(n); - for (int i = 0; i < n; ++i) r.push_back({U.active_headers[i], U.active_types[i]}); - return r; -} - -void draw_viz_selector(State& st) { - int n_modes = 0; - const ViewMode* modes = all_view_modes(&n_modes); - - // Right-align: reserve "View: [combo] [Fit] [Lock] [Config] [+ Viz]" - const float combo_w = 200.0f; - const float total_w = combo_w + 50.0f + 280.0f; - float right_edge = ImGui::GetWindowContentRegionMax().x; - float target_x = right_edge - total_w; - float min_x = ImGui::GetCursorPosX() + 20.0f; // do not overlap breadcrumb - if (target_x < min_x) target_x = min_x; - ImGui::SameLine(); - ImGui::SetCursorPosX(target_x); - - ImGui::TextDisabled("View:"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(combo_w); - if (ImGui::BeginCombo("##viz_mode", view_mode_label(st.display))) { - for (int i = 0; i < n_modes; ++i) { - bool sel = (modes[i] == st.display); - if (ImGui::Selectable(view_mode_label(modes[i]), sel)) { - ViewMode nm = modes[i]; - if (nm != st.display) { - st.display = nm; - if (view_mode_needs_aggregation(nm)) { - auto_promote_aggregated(st); - } - } - } - if (sel) ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - ImGui::SameLine(); - if (ImGui::SmallButton("Fit##viz_fit")) { - st.viz_config.fit_request = true; - } - ImGui::SameLine(); - bool locked = st.viz_config.locked; - if (locked) { - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(180, 60, 60, 230)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(200, 80, 80, 240)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(150, 40, 40, 240)); - } - if (ImGui::SmallButton(locked ? "Locked##viz_lock" : "Lock##viz_lock")) { - st.viz_config.locked = !st.viz_config.locked; - } - if (locked) ImGui::PopStyleColor(3); - ImGui::SameLine(); - if (ImGui::SmallButton("Config##viz_cfg")) { - ImGui::OpenPopup("##viz_cfg_popup"); - } - ImGui::SameLine(); - if (ImGui::SmallButton("Ask AI##ask_open")) { - auto& U2 = ui(); - U2.ask_open = true; - U2.ask_busy = false; - U2.ask_error.clear(); - U2.ask_status.clear(); - U2.ask_response_code.clear(); - U2.ask_response_raw.clear(); - U2.ask_current_tql = tql::emit(st, - std::vector(), // emit headers stage 0 (caller fill si necesario) - std::vector()); - } - ImGui::SameLine(); - if (ImGui::SmallButton("+ Viz##viz_add")) { - VizPanel p; - p.display = ViewMode::Bar; - if (view_mode_needs_aggregation(p.display)) { - auto_promote_aggregated(st); - } - st.extra_panels.push_back(p); - } - draw_viz_config_popup(st); - ImGui::NewLine(); -} - -// --------------------------------------------------------------------------- -// Join chips (fase 9 — solo visible si hay joinables). -// --------------------------------------------------------------------------- -void draw_joins_chips(State& st, const std::vector& joinables, - const std::vector& main_headers) { - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 80, 130, 90, 220)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(100, 160, 110, 240)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 60, 110, 70, 220)); - ImGui::TextDisabled("Joins:"); - ImGui::SameLine(); - - int remove_idx = -1; - for (size_t i = 0; i < st.joins.size(); ++i) { - const auto& jn = st.joins[i]; - char lbl[256]; - std::string on_str; - for (size_t k = 0; k < jn.on.size(); ++k) { - if (k) on_str += ","; - on_str += jn.on[k].first + "=" + jn.on[k].second; - } - std::snprintf(lbl, sizeof(lbl), "%s <- %s on %s (%s)##join_%zu", - jn.alias.empty() ? "_" : jn.alias.c_str(), - jn.source.c_str(), - on_str.c_str(), - join_strategy_label(jn.strategy), - i); - ImGui::Button(lbl); - ImGui::SameLine(); - char xlbl[32]; std::snprintf(xlbl, sizeof(xlbl), "x##rm_join_%zu", i); - if (ImGui::SmallButton(xlbl)) remove_idx = (int)i; - ImGui::SameLine(); - } - if (remove_idx >= 0) st.joins.erase(st.joins.begin() + remove_idx); - - if (ImGui::SmallButton("+##add_join")) { - ImGui::OpenPopup("##add_join_popup"); - } - ImGui::PopStyleColor(3); - - // Popup add - static int pick_source_idx = 0; - static char pick_alias[64] = ""; - static int pick_strategy = 0; - static int pick_left_col = 0; - static int pick_right_col = 0; - if (ImGui::BeginPopup("##add_join_popup")) { - ImGui::Text("Add join"); - ImGui::SetNextItemWidth(180); - if (ImGui::BeginCombo("source", joinables[pick_source_idx].name.c_str())) { - for (int k = 0; k < (int)joinables.size(); ++k) { - bool sel = (k == pick_source_idx); - if (ImGui::Selectable(joinables[k].name.c_str(), sel)) { - pick_source_idx = k; - pick_right_col = 0; - if (pick_alias[0] == 0) - std::snprintf(pick_alias, sizeof(pick_alias), "%s", joinables[k].name.c_str()); - } - } - ImGui::EndCombo(); - } - ImGui::SetNextItemWidth(180); - ImGui::InputText("alias", pick_alias, sizeof(pick_alias)); - - const char* strategies[] = {"left", "inner", "right", "full"}; - ImGui::SetNextItemWidth(120); - ImGui::Combo("strategy", &pick_strategy, strategies, IM_ARRAYSIZE(strategies)); - - // left col combo (de main_headers) - ImGui::SetNextItemWidth(180); - const char* lcur = (pick_left_col >= 0 && pick_left_col < (int)main_headers.size()) - ? main_headers[pick_left_col].c_str() : "?"; - if (ImGui::BeginCombo("left col", lcur)) { - for (int k = 0; k < (int)main_headers.size(); ++k) { - bool sel = (k == pick_left_col); - if (ImGui::Selectable(main_headers[k].c_str(), sel)) pick_left_col = k; - } - ImGui::EndCombo(); - } - - // right col combo (de joinables[pick_source_idx].headers) - const TableInput& src = joinables[pick_source_idx]; - const char* rcur = (pick_right_col >= 0 && pick_right_col < (int)src.headers.size()) - ? src.headers[pick_right_col].c_str() : "?"; - ImGui::SetNextItemWidth(180); - if (ImGui::BeginCombo("right col", rcur)) { - for (int k = 0; k < (int)src.headers.size(); ++k) { - bool sel = (k == pick_right_col); - if (ImGui::Selectable(src.headers[k].c_str(), sel)) pick_right_col = k; - } - ImGui::EndCombo(); - } - - ImGui::Separator(); - if (ImGui::SmallButton("Add")) { - Join jn; - jn.alias = pick_alias; - jn.source = src.name; - jn.on.push_back({main_headers[pick_left_col], src.headers[pick_right_col]}); - jn.strategy = (JoinStrategy)pick_strategy; - st.joins.push_back(jn); - pick_alias[0] = 0; - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::SmallButton("Cancel")) ImGui::CloseCurrentPopup(); - - ImGui::EndPopup(); - } - ImGui::NewLine(); -} - -// --------------------------------------------------------------------------- -// Filter chips para el stage activo. eff_headers/eff_cols son del INPUT del -// stage activo (= orig+derived para stage 0; output del stage previo para 1+). -// --------------------------------------------------------------------------- -void draw_filter_chips(Stage& stg, const char* const* eff_headers, int eff_cols, - const std::vector& eff_types) { - auto& U = ui(); - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(120, 60, 170, 220)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(150, 85, 200, 240)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 95, 45, 140, 240)); - if (ImGui::SmallButton("+##addfilter_btn")) ImGui::OpenPopup("##addfilter"); - ImGui::PopStyleColor(3); - ImGui::SameLine(); - - // Presets (fase 10): menu con Last7/30/90d (cols Date), ExcludeNulls (any), - // NonZero (cols numericas). Apply append a stg.filters via build_preset_filters. - if (ImGui::SmallButton("Presets##fpresets")) ImGui::OpenPopup("##presets_menu"); - if (ImGui::BeginPopup("##presets_menu")) { - int first_date = -1, first_num = -1; - for (int c = 0; c < eff_cols && c < (int)eff_types.size(); ++c) { - if (first_date < 0 && eff_types[c] == ColumnType::Date) first_date = c; - if (first_num < 0 && (eff_types[c] == ColumnType::Int || - eff_types[c] == ColumnType::Float)) first_num = c; - } - auto apply_preset = [&](FilterPreset p, int col) { - auto fs = build_preset_filters(p, col, today_iso()); - for (auto& f : fs) stg.filters.push_back(f); - }; - if (first_date >= 0) { - char l1[96], l2[96], l3[96]; - std::snprintf(l1, sizeof(l1), "Last 7 days on \"%s\"", eff_headers[first_date]); - std::snprintf(l2, sizeof(l2), "Last 30 days on \"%s\"", eff_headers[first_date]); - std::snprintf(l3, sizeof(l3), "Last 90 days on \"%s\"", eff_headers[first_date]); - if (ImGui::MenuItem(l1)) apply_preset(FilterPreset::Last7d, first_date); - if (ImGui::MenuItem(l2)) apply_preset(FilterPreset::Last30d, first_date); - if (ImGui::MenuItem(l3)) apply_preset(FilterPreset::Last90d, first_date); - ImGui::Separator(); - } - if (ImGui::BeginMenu("Exclude nulls in...")) { - for (int c = 0; c < eff_cols; ++c) { - if (ImGui::MenuItem(eff_headers[c])) apply_preset(FilterPreset::ExcludeNulls, c); - } - ImGui::EndMenu(); - } - if (first_num >= 0) { - if (ImGui::BeginMenu("Non-zero in...")) { - for (int c = 0; c < eff_cols && c < (int)eff_types.size(); ++c) { - if (eff_types[c] == ColumnType::Int || eff_types[c] == ColumnType::Float) { - if (ImGui::MenuItem(eff_headers[c])) apply_preset(FilterPreset::NonZero, c); - } - } - ImGui::EndMenu(); - } - } - ImGui::EndPopup(); - } - ImGui::SameLine(); - - if (stg.filters.empty()) { - ImGui::TextDisabled("Sin filtros."); - return; - } - for (size_t i = 0; i < stg.filters.size(); ) { - const auto& f = stg.filters[i]; - const char* hdr = (f.col >= 0 && f.col < eff_cols) ? eff_headers[f.col] : "?"; - char buf[256]; - std::snprintf(buf, sizeof(buf), "%s %s %s x##chip%zu", - hdr, op_label(f.op), f.value.c_str(), i); - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(120, 60, 170, 220)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(150, 85, 200, 240)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 95, 45, 140, 240)); - bool clicked = ImGui::SmallButton(buf); - ImGui::PopStyleColor(3); - // Click derecho: edit - if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - U.edit_chip_kind = 1; - U.edit_chip_idx = (int)i; - U.edit_col_idx = f.col; - U.edit_op = (int)f.op; - U.edit_value = f.value; - ImGui::OpenPopup("##edit_filter"); - } - if (clicked) { stg.filters.erase(stg.filters.begin() + i); continue; } - ImGui::SameLine(); - ++i; - } - ImGui::NewLine(); -} - -// Chips de breakout (stage > 0). -void draw_breakout_chips(Stage& stg, const char* const* in_headers, int in_cols, - const std::vector& in_types) { - auto& U = ui(); - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 60, 160, 170, 220)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 80, 190, 200, 240)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 40, 130, 140, 240)); - if (ImGui::SmallButton("+##addbreakout_btn")) ImGui::OpenPopup("##addbreakout"); - ImGui::PopStyleColor(3); - ImGui::SameLine(); - - if (stg.breakouts.empty()) { - ImGui::TextDisabled("Group by: ninguna col."); - return; - } - for (size_t i = 0; i < stg.breakouts.size(); ) { - std::string col_name; - DateGranularity g = parse_breakout_granularity(stg.breakouts[i], col_name); - - // Resolve col index para lookup de tipo. - int col_idx = -1; - for (int c = 0; c < in_cols; ++c) { - if (std::strcmp(in_headers[c], col_name.c_str()) == 0) { col_idx = c; break; } - } - bool is_date_col = (col_idx >= 0 && col_idx < (int)in_types.size() - && in_types[col_idx] == ColumnType::Date); - - char buf[256]; - std::snprintf(buf, sizeof(buf), "%s x##bk%zu", stg.breakouts[i].c_str(), i); - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 60, 160, 170, 220)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 80, 190, 200, 240)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 40, 130, 140, 240)); - bool clicked = ImGui::SmallButton(buf); - ImGui::PopStyleColor(3); - if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - U.edit_chip_kind = 2; - U.edit_chip_idx = (int)i; - U.edit_col_idx = (col_idx >= 0) ? col_idx : 0; - ImGui::OpenPopup("##edit_breakout"); - } - if (clicked) { stg.breakouts.erase(stg.breakouts.begin() + i); continue; } - - // Granularity combo inline cuando col Date (fase 10). - if (is_date_col) { - ImGui::SameLine(); - const char* preview = (g == DateGranularity::None) - ? "(raw)" : date_granularity_token(g); - char combo_id[32]; - std::snprintf(combo_id, sizeof(combo_id), "##gran%zu", i); - ImGui::SetNextItemWidth(72); - if (ImGui::BeginCombo(combo_id, preview)) { - DateGranularity opts[] = { - DateGranularity::None, - DateGranularity::Year, - DateGranularity::Month, - DateGranularity::Week, - DateGranularity::Day, - DateGranularity::Hour, - }; - for (auto o : opts) { - const char* lbl = (o == DateGranularity::None) - ? "(raw)" : date_granularity_token(o); - if (ImGui::Selectable(lbl, o == g)) { - stg.breakouts[i] = compose_breakout(col_name, o); - } - } - ImGui::EndCombo(); - } - } - - ImGui::SameLine(); - ++i; - } - ImGui::NewLine(); -} - -const char* agg_fn_label(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 "?"; -} - -void draw_aggregation_chips(Stage& stg, const char* const* in_headers, int in_cols) { - auto& U = ui(); - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 40, 140, 60, 220)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 60, 170, 85, 240)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 30, 110, 45, 240)); - if (ImGui::SmallButton("+##addagg_btn")) ImGui::OpenPopup("##addagg"); - ImGui::PopStyleColor(3); - ImGui::SameLine(); - - if (stg.aggregations.empty()) { - ImGui::TextDisabled("Aggregations: ninguna."); - return; - } - for (size_t i = 0; i < stg.aggregations.size(); ) { - const auto& a = stg.aggregations[i]; - char buf[256]; - if (a.fn == AggFn::Count) { - std::snprintf(buf, sizeof(buf), "count x##ag%zu", i); - } else if (a.fn == AggFn::Percentile) { - std::snprintf(buf, sizeof(buf), "percentile(%s, %g) x##ag%zu", - a.col.c_str(), a.arg, i); - } else { - std::snprintf(buf, sizeof(buf), "%s(%s) x##ag%zu", - agg_fn_label(a.fn), a.col.c_str(), i); - } - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 40, 140, 60, 220)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 60, 170, 85, 240)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 30, 110, 45, 240)); - bool clicked = ImGui::SmallButton(buf); - ImGui::PopStyleColor(3); - if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - U.edit_chip_kind = 3; - U.edit_chip_idx = (int)i; - U.edit_agg_fn = (int)a.fn; - U.edit_agg_arg = a.arg; - U.edit_col_idx = 0; - for (int c = 0; c < in_cols; ++c) { - if (std::strcmp(in_headers[c], a.col.c_str()) == 0) { - U.edit_col_idx = c; break; - } - } - ImGui::OpenPopup("##edit_agg"); - } - if (clicked) { stg.aggregations.erase(stg.aggregations.begin() + i); continue; } - ImGui::SameLine(); - ++i; - } - (void)in_headers; (void)in_cols; - ImGui::NewLine(); -} - -void draw_sort_chips(Stage& stg) { - auto& U = ui(); - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(220, 130, 50, 230)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(240, 155, 75, 245)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(180, 100, 30, 240)); - if (ImGui::SmallButton("+##addsort_btn")) ImGui::OpenPopup("##addsort"); - ImGui::PopStyleColor(3); - ImGui::SameLine(); - - if (stg.sorts.empty()) { - ImGui::TextDisabled("Sort: ninguno."); - return; - } - int erase_idx = -1; - int drag_src = -1; - int drag_dst = -1; - for (size_t i = 0; i < stg.sorts.size(); ++i) { - const auto& sc = stg.sorts[i]; - char buf[256]; - std::snprintf(buf, sizeof(buf), "%zu. %s %s x##srt%zu", - i + 1, sc.col.c_str(), sc.desc ? "desc" : "asc", i); - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(220, 130, 50, 230)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(240, 155, 75, 245)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(180, 100, 30, 240)); - bool clicked = ImGui::SmallButton(buf); - ImGui::PopStyleColor(3); - - // Drag source: prioridad multi-sort reorderable. - if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) { - int idx = (int)i; - ImGui::SetDragDropPayload("##sortreorder", &idx, sizeof(int)); - ImGui::Text("Move sort #%zu", i + 1); - ImGui::EndDragDropSource(); - } - if (ImGui::BeginDragDropTarget()) { - if (const ImGuiPayload* p = ImGui::AcceptDragDropPayload("##sortreorder")) { - drag_src = *(const int*)p->Data; - drag_dst = (int)i; - } - ImGui::EndDragDropTarget(); - } - if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - U.edit_chip_kind = 4; - U.edit_chip_idx = (int)i; - U.edit_value = sc.col; - U.edit_sort_desc = sc.desc; - ImGui::OpenPopup("##edit_sort"); - } - if (clicked) erase_idx = (int)i; - ImGui::SameLine(); - } - ImGui::NewLine(); - - if (drag_src >= 0 && drag_dst >= 0 && drag_src != drag_dst && - drag_src < (int)stg.sorts.size() && drag_dst < (int)stg.sorts.size()) - { - SortClause moved = std::move(stg.sorts[drag_src]); - stg.sorts.erase(stg.sorts.begin() + drag_src); - int insert_at = (drag_src < drag_dst) ? drag_dst : drag_dst; - if (insert_at > (int)stg.sorts.size()) insert_at = (int)stg.sorts.size(); - stg.sorts.insert(stg.sorts.begin() + insert_at, std::move(moved)); - } else if (erase_idx >= 0 && erase_idx < (int)stg.sorts.size()) { - stg.sorts.erase(stg.sorts.begin() + erase_idx); - } -} - -// ---- Edit chip popups: click derecho sobre chip abre popup. ---- -// Header click handler: -// click: si col ya esta en sorts -> cicla su direccion asc/desc/off. -// sino -> append {col, asc} al final (multi-sort por defecto). -// shift+click: reset. Reemplaza sorts con {col, asc} (sort unico). -void apply_header_sort_click(Stage& stg, const std::string& col_name, bool shift) { - if (shift) { - stg.sorts.clear(); - stg.sorts.push_back({col_name, false}); - return; - } - int idx = -1; - for (size_t i = 0; i < stg.sorts.size(); ++i) { - if (stg.sorts[i].col == col_name) { idx = (int)i; break; } - } - if (idx < 0) { - stg.sorts.push_back({col_name, false}); - } else { - if (!stg.sorts[idx].desc) stg.sorts[idx].desc = true; - else stg.sorts.erase(stg.sorts.begin() + idx); - } -} - -void draw_edit_filter_popup(Stage& stg, const char* const* headers, int n_cols, - const std::vector& types) { - auto& U = ui(); - if (!ImGui::BeginPopup("##edit_filter")) return; - if (U.edit_chip_idx >= 0 && U.edit_chip_idx < (int)stg.filters.size()) { - auto& f = stg.filters[U.edit_chip_idx]; - ImGui::SetNextItemWidth(200); - const char* cur = (U.edit_col_idx >= 0 && U.edit_col_idx < n_cols) - ? headers[U.edit_col_idx] : "?"; - if (ImGui::BeginCombo("col", cur)) { - for (int c = 0; c < n_cols; ++c) { - bool sel = (U.edit_col_idx == c); - if (ImGui::Selectable(headers[c], sel)) U.edit_col_idx = c; - } - ImGui::EndCombo(); - } - ColumnType t = (U.edit_col_idx >= 0 && U.edit_col_idx < (int)types.size()) - ? types[U.edit_col_idx] : ColumnType::String; - auto ops = ops_for_type(t); - ImGui::SetNextItemWidth(140); - if (ImGui::BeginCombo("op", op_label((Op)U.edit_op))) { - for (auto o : ops) { - bool sel = ((int)o == U.edit_op); - if (ImGui::Selectable(op_label(o), sel)) U.edit_op = (int)o; - } - ImGui::EndCombo(); - } - char vbuf[256] = {0}; - std::snprintf(vbuf, sizeof(vbuf), "%s", U.edit_value.c_str()); - ImGui::SetNextItemWidth(220); - if (ImGui::InputText("value", vbuf, sizeof(vbuf))) U.edit_value = vbuf; - if (ImGui::Button("Save")) { - f.col = U.edit_col_idx; - f.op = (Op)U.edit_op; - f.value = U.edit_value; - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); -} - -void draw_edit_breakout_popup(Stage& stg, const char* const* headers, int n_cols) { - auto& U = ui(); - if (!ImGui::BeginPopup("##edit_breakout")) return; - if (U.edit_chip_idx >= 0 && U.edit_chip_idx < (int)stg.breakouts.size()) { - ImGui::SetNextItemWidth(240); - const char* cur = (U.edit_col_idx >= 0 && U.edit_col_idx < n_cols) - ? headers[U.edit_col_idx] : "?"; - if (ImGui::BeginCombo("col", cur)) { - for (int c = 0; c < n_cols; ++c) { - bool sel = (U.edit_col_idx == c); - if (ImGui::Selectable(headers[c], sel)) U.edit_col_idx = c; - } - ImGui::EndCombo(); - } - if (ImGui::Button("Save")) { - if (U.edit_col_idx >= 0 && U.edit_col_idx < n_cols) - stg.breakouts[U.edit_chip_idx] = headers[U.edit_col_idx]; - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); -} - -void draw_edit_agg_popup(Stage& stg, const char* const* headers, int n_cols) { - auto& U = ui(); - if (!ImGui::BeginPopup("##edit_agg")) return; - if (U.edit_chip_idx >= 0 && U.edit_chip_idx < (int)stg.aggregations.size()) { - const AggFn all_fns[] = {AggFn::Count, AggFn::Sum, AggFn::Avg, AggFn::Min, AggFn::Max, - AggFn::Distinct, AggFn::Stddev, AggFn::Median, - AggFn::P25, AggFn::P75, AggFn::P90, AggFn::P99, - AggFn::Percentile}; - ImGui::SetNextItemWidth(160); - if (ImGui::BeginCombo("fn", agg_fn_label((AggFn)U.edit_agg_fn))) { - for (auto f : all_fns) { - bool sel = ((int)f == U.edit_agg_fn); - if (ImGui::Selectable(agg_fn_label(f), sel)) U.edit_agg_fn = (int)f; - } - ImGui::EndCombo(); - } - if ((AggFn)U.edit_agg_fn != AggFn::Count) { - ImGui::SetNextItemWidth(200); - const char* cur = (U.edit_col_idx >= 0 && U.edit_col_idx < n_cols) - ? headers[U.edit_col_idx] : "?"; - if (ImGui::BeginCombo("col", cur)) { - for (int c = 0; c < n_cols; ++c) { - bool sel = (U.edit_col_idx == c); - if (ImGui::Selectable(headers[c], sel)) U.edit_col_idx = c; - } - ImGui::EndCombo(); - } - } - if ((AggFn)U.edit_agg_fn == AggFn::Percentile) { - float v = (float)U.edit_agg_arg; - ImGui::SetNextItemWidth(140); - if (ImGui::InputFloat("p (0..1)", &v, 0.05f, 0.1f, "%.2f")) - U.edit_agg_arg = v; - } - if (ImGui::Button("Save")) { - auto& a = stg.aggregations[U.edit_chip_idx]; - a.fn = (AggFn)U.edit_agg_fn; - if (a.fn != AggFn::Count && U.edit_col_idx >= 0 && U.edit_col_idx < n_cols) - a.col = headers[U.edit_col_idx]; - else if (a.fn == AggFn::Count) a.col.clear(); - a.arg = U.edit_agg_arg; - a.alias.clear(); - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); -} - -void draw_edit_sort_popup(Stage& stg, const char* const* headers, int n_cols) { - auto& U = ui(); - if (!ImGui::BeginPopup("##edit_sort")) return; - if (U.edit_chip_idx >= 0 && U.edit_chip_idx < (int)stg.sorts.size()) { - ImGui::SetNextItemWidth(240); - if (ImGui::BeginCombo("col", U.edit_value.c_str())) { - for (int c = 0; c < n_cols; ++c) { - bool sel = (U.edit_value == headers[c]); - if (ImGui::Selectable(headers[c], sel)) U.edit_value = headers[c]; - } - ImGui::EndCombo(); - } - if (ImGui::RadioButton("asc", !U.edit_sort_desc)) U.edit_sort_desc = false; - ImGui::SameLine(); - if (ImGui::RadioButton("desc", U.edit_sort_desc)) U.edit_sort_desc = true; - if (ImGui::Button("Save")) { - auto& sc = stg.sorts[U.edit_chip_idx]; - sc.col = U.edit_value; - sc.desc = U.edit_sort_desc; - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); -} - -void maybe_recompute_stats(const char* const* cells, int rows, int orig_cols, - int eff_cols, const std::vector& filters, - const std::vector& visible, - const std::vector& src_for_eff) -{ - auto& U = ui(); - if (!U.stats_mode) return; - size_t fh = filters_hash(filters); - bool ds_changed = (cells != U.last_cells || rows != U.last_rows || - eff_cols != U.last_eff_cols || - (int)U.stats_cache.size() != eff_cols); - bool fl_changed = (fh != U.last_filter_h || (int)visible.size() != U.last_visible); - if (!ds_changed && !fl_changed) return; - U.stats_cache.resize(eff_cols); - const int* idx = visible.empty() ? nullptr : visible.data(); - int n = (int)visible.size(); - for (int c = 0; c < eff_cols; ++c) { - int src = src_for_eff[c]; - U.stats_cache[c] = compute_column_stats(cells, rows, orig_cols, src, - 100000, idx, n); - } - U.last_cells = cells; - U.last_rows = rows; - U.last_eff_cols = eff_cols; - U.last_filter_h = fh; - U.last_visible = (int)visible.size(); -} - -bool draw_typed_ops(ColumnType t, Op& out) { - auto ops = ops_for_type(t); - for (size_t i = 0; i < ops.size(); ++i) { - if (i % 5 != 0) ImGui::SameLine(); - if (ImGui::SmallButton(op_label(ops[i]))) { out = ops[i]; return true; } - } - return false; -} - -bool type_supports_range(ColumnType t) { - return t == ColumnType::Int || t == ColumnType::Float || t == ColumnType::Date; -} - -void draw_add_filter_popup(Stage& stg, const char* const* eff_headers_arr, int eff_cols, - const std::vector& eff_types) -{ - auto& U = ui(); - if (!ImGui::BeginPopup("##addfilter")) return; - if (U.addf_col < 0 || U.addf_col >= eff_cols) U.addf_col = 0; - ImGui::SetNextItemWidth(220); - if (ImGui::BeginCombo("col", eff_headers_arr[U.addf_col])) { - for (int c = 0; c < eff_cols; ++c) { - char it[160]; - std::snprintf(it, sizeof(it), "%s %s", - column_type_icon(eff_types[c]), eff_headers_arr[c]); - bool sel = (U.addf_col == c); - if (ImGui::Selectable(it, sel)) U.addf_col = c; - } - ImGui::EndCombo(); - } - ColumnType t = eff_types[U.addf_col]; - ImGui::TextDisabled("type: %s %s", column_type_icon(t), column_type_name(t)); - - bool can_range = type_supports_range(t); - if (can_range) ImGui::Checkbox("Range (min/max)", &U.addf_range); - else U.addf_range = false; - - if (!U.addf_range) { - char buf[256] = {0}; - std::snprintf(buf, sizeof(buf), "%s", U.addf_val.c_str()); - ImGui::SetNextItemWidth(220); - if (ImGui::InputText("val", buf, sizeof(buf))) U.addf_val = buf; - Op picked; - if (draw_typed_ops(t, picked)) { - stg.filters.push_back({U.addf_col, picked, U.addf_val}); - U.addf_val.clear(); - ImGui::CloseCurrentPopup(); - } - } else { - char lo[128] = {0}, hi[128] = {0}; - std::snprintf(lo, sizeof(lo), "%s", U.addf_lo.c_str()); - std::snprintf(hi, sizeof(hi), "%s", U.addf_hi.c_str()); - ImGui::SetNextItemWidth(100); - if (ImGui::InputText("min", lo, sizeof(lo))) U.addf_lo = lo; - ImGui::SameLine(); - ImGui::SetNextItemWidth(100); - if (ImGui::InputText("max", hi, sizeof(hi))) U.addf_hi = hi; - ImGui::SameLine(); - if (ImGui::SmallButton("Add range")) { - if (!U.addf_lo.empty()) stg.filters.push_back({U.addf_col, Op::Gte, U.addf_lo}); - if (!U.addf_hi.empty()) stg.filters.push_back({U.addf_col, Op::Lte, U.addf_hi}); - U.addf_lo.clear(); U.addf_hi.clear(); - ImGui::CloseCurrentPopup(); - } - } - ImGui::EndPopup(); -} - -void draw_add_breakout_popup(Stage& stg, const char* const* in_headers, int in_cols, - const std::vector& in_types, - const char* const* in_cells, int in_rows) { - auto& U = ui(); - if (!ImGui::BeginPopup("##addbreakout")) return; - if (U.brk_picker_col < 0 || U.brk_picker_col >= in_cols) U.brk_picker_col = 0; - ImGui::SetNextItemWidth(220); - if (ImGui::BeginCombo("col##bkcol", in_headers[U.brk_picker_col])) { - for (int c = 0; c < in_cols; ++c) { - char it[160]; - std::snprintf(it, sizeof(it), "%s %s", - column_type_icon(in_types[c]), in_headers[c]); - bool sel = (U.brk_picker_col == c); - if (ImGui::Selectable(it, sel)) U.brk_picker_col = c; - } - ImGui::EndCombo(); - } - if (ImGui::Button("Add##bk")) { - int c = U.brk_picker_col; - std::string col = in_headers[c]; - // Fase 10: si col es Date, auto-detect granularidad via rango lexical - // (ISO YYYY-MM-DD ordena bien). Default Day si rango invalido. - if (c >= 0 && c < (int)in_types.size() && in_types[c] == ColumnType::Date) { - std::string lo, hi; - column_min_max(in_cells, in_rows, in_cols, c, lo, hi); - DateGranularity g = auto_date_granularity(lo, hi); - stg.breakouts.emplace_back(compose_breakout(col, g)); - } else { - stg.breakouts.emplace_back(col); - } - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); -} - -void draw_add_aggregation_popup(Stage& stg, const char* const* in_headers, int in_cols, - const std::vector& in_types) { - auto& U = ui(); - if (!ImGui::BeginPopup("##addagg")) return; - - AggFn cur_fn = (AggFn)U.agg_picker_fn; - ImGui::SetNextItemWidth(160); - if (ImGui::BeginCombo("fn##aggfn", agg_fn_label(cur_fn))) { - AggFn all[] = {AggFn::Count, AggFn::Sum, AggFn::Avg, AggFn::Min, AggFn::Max, - AggFn::Distinct, AggFn::Stddev, AggFn::Median, - AggFn::P25, AggFn::P75, AggFn::P90, AggFn::P99, - AggFn::Percentile}; - for (AggFn f : all) { - bool sel = (f == cur_fn); - if (ImGui::Selectable(agg_fn_label(f), sel)) U.agg_picker_fn = (int)f; - } - ImGui::EndCombo(); - } - if (cur_fn != AggFn::Count) { - if (U.agg_picker_col < 0 || U.agg_picker_col >= in_cols) U.agg_picker_col = 0; - ImGui::SetNextItemWidth(220); - if (ImGui::BeginCombo("col##aggcol", in_headers[U.agg_picker_col])) { - for (int c = 0; c < in_cols; ++c) { - char it[160]; - std::snprintf(it, sizeof(it), "%s %s", - column_type_icon(in_types[c]), in_headers[c]); - bool sel = (U.agg_picker_col == c); - if (ImGui::Selectable(it, sel)) U.agg_picker_col = c; - } - ImGui::EndCombo(); - } - } - if (cur_fn == AggFn::Percentile) { - double v = U.agg_picker_arg; - ImGui::SetNextItemWidth(120); - if (ImGui::InputDouble("p (0..1)", &v, 0.05, 0.1, "%.2f")) { - if (v < 0) v = 0; if (v > 1) v = 1; - U.agg_picker_arg = v; - } - } - if (ImGui::Button("Add##ag")) { - Aggregation a; - a.fn = cur_fn; - a.col = (cur_fn == AggFn::Count) ? "" : std::string(in_headers[U.agg_picker_col]); - a.arg = (cur_fn == AggFn::Percentile) ? U.agg_picker_arg : 0.0; - stg.aggregations.push_back(a); - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); -} - -void draw_add_sort_popup(Stage& stg, const char* const* in_headers, int in_cols, - const std::vector& in_types) { - auto& U = ui(); - if (!ImGui::BeginPopup("##addsort")) return; - if (U.sort_picker_col < 0 || U.sort_picker_col >= in_cols) U.sort_picker_col = 0; - ImGui::SetNextItemWidth(220); - if (ImGui::BeginCombo("col##sortcol", in_headers[U.sort_picker_col])) { - for (int c = 0; c < in_cols; ++c) { - char it[160]; - std::snprintf(it, sizeof(it), "%s %s", - column_type_icon(in_types[c]), in_headers[c]); - bool sel = (U.sort_picker_col == c); - if (ImGui::Selectable(it, sel)) U.sort_picker_col = c; - } - ImGui::EndCombo(); - } - ImGui::Checkbox("desc", &U.sort_picker_desc); - if (ImGui::Button("Add##srt")) { - SortClause sc; - sc.col = in_headers[U.sort_picker_col]; - sc.desc = U.sort_picker_desc; - stg.sorts.push_back(sc); - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); -} - -void draw_header_menu(State& st, Stage& stg, int col, - const char* const* eff_headers_arr, int eff_cols, - const std::vector& eff_types, - int orig_cols, bool is_raw_stage) -{ - auto& U = ui(); - ColumnType t = eff_types[col]; - - if (ImGui::MenuItem("Sort ascending")) { - stg.sorts.clear(); - stg.sorts.push_back({eff_headers_arr[col], false}); - } - if (ImGui::MenuItem("Sort descending")) { - stg.sorts.clear(); - stg.sorts.push_back({eff_headers_arr[col], true}); - } - if (!stg.sorts.empty() && ImGui::MenuItem("Clear sort")) stg.sorts.clear(); - ImGui::Separator(); - - auto& fbuf = U.filter_inputs[col]; - fbuf.resize(256, '\0'); - if (ImGui::BeginMenu("Filter...")) { - ImGui::SetNextItemWidth(220); - ImGui::InputText("##filterval", fbuf.data(), fbuf.size()); - std::string val(fbuf.c_str()); - auto ops = ops_for_type(t); - for (size_t i = 0; i < ops.size(); ++i) { - if (i % 5 != 0) ImGui::SameLine(); - if (ImGui::SmallButton(op_label(ops[i]))) { - stg.filters.push_back({col, ops[i], val}); - ImGui::CloseCurrentPopup(); - } - } - ImGui::EndMenu(); - } - - // Change type / derived solo en stage 0. - if (is_raw_stage) { - if (ImGui::BeginMenu("Change type")) { - const ColumnType types[] = { - ColumnType::String, ColumnType::Int, ColumnType::Float, - ColumnType::Bool, ColumnType::Date, ColumnType::Json - }; - for (auto nt : types) { - char lab[64]; - std::snprintf(lab, sizeof(lab), "%s %s", - column_type_icon(nt), column_type_name(nt)); - if (ImGui::MenuItem(lab)) { - DerivedColumn d; - d.source_col = (col < orig_cols) ? col : stg.derived[col - orig_cols].source_col; - d.type = nt; - d.name = std::string(eff_headers_arr[col]) + "_" + column_type_name(nt); - stg.derived.push_back(d); - } - } - ImGui::EndMenu(); - } - } - - if (ImGui::BeginMenu("Conditional color")) { - auto& vbuf = U.color_value_inputs[col]; - vbuf.resize(256, '\0'); - if (U.color_picker_vals.find(col) == U.color_picker_vals.end()) - U.color_picker_vals[col] = ImVec4(0.85f, 0.40f, 0.30f, 0.60f); - ImVec4& cv = U.color_picker_vals[col]; - ImGui::SetNextItemWidth(180); - ImGui::InputText("equals", vbuf.data(), vbuf.size()); - ImGui::ColorEdit4("color", &cv.x, ImGuiColorEditFlags_NoInputs); - if (ImGui::Button("Apply")) { - ImU32 c = ImGui::ColorConvertFloat4ToU32(cv); - st.color_rules.push_back({col, std::string(vbuf.c_str()), (unsigned int)c}); - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("Clear col")) { - for (size_t i = 0; i < st.color_rules.size();) { - if (st.color_rules[i].col == col) st.color_rules.erase(st.color_rules.begin() + i); - else ++i; - } - } - ImGui::EndMenu(); - } - - if (ImGui::MenuItem("Hide column")) st.col_visible[col] = false; - - if (is_raw_stage && col >= orig_cols && ImGui::MenuItem("Remove derived column")) { - int k = col - orig_cols; - stg.derived.erase(stg.derived.begin() + k); - } - - ImGui::Separator(); - if (ImGui::BeginMenu("Columns")) { - for (int k = 0; k < eff_cols; ++k) { - bool v = st.col_visible[k]; - char lab[160]; - std::snprintf(lab, sizeof(lab), "%s %s", - column_type_icon(eff_types[k]), eff_headers_arr[k]); - if (ImGui::Checkbox(lab, &v)) st.col_visible[k] = v; - } - if (ImGui::MenuItem("Show all")) { - for (int k = 0; k < eff_cols; ++k) st.col_visible[k] = true; - } - ImGui::EndMenu(); - } -} - -// --------------------------------------------------------------------------- -// Drill-down: anade un filter al stage previo y cambia active a stage previo. -// `col_name` y `value` se aplican como un Filter Op::Eq sobre el stage N-1. -// --------------------------------------------------------------------------- -void drill_into(State& st, int from_stage, - const std::string& col_name, const std::string& value, - const std::vector& prev_input_headers) -{ - if (from_stage <= 0 || from_stage >= (int)st.stages.size()) return; - int target = from_stage - 1; - int ci = -1; - for (size_t i = 0; i < prev_input_headers.size(); ++i) { - if (prev_input_headers[i] == col_name) { ci = (int)i; break; } - } - if (ci < 0) return; - - // Fase 10: graba step en drill_back, limpia forward (rama nueva). - DrillStep step; - step.target_stage = target; - step.filter_pos = (int)st.stages[target].filters.size(); - step.prev_active_stage = st.active_stage; - step.added = make_drill_filter(ci, value); - apply_drill_step(st, step); - auto& U = ui(); - U.drill_back.push_back(step); - U.drill_forward.clear(); -} - -} // anon namespace - -void render(const char* id, - const std::vector& tables, - State& st, - bool show_chrome) -{ - if (tables.empty()) return; - int main_idx = resolve_main_idx(tables, st.main_source); - if (main_idx < 0) return; - - // Construir headers ptrs desde main table. - const TableInput& main_t = tables[(size_t)main_idx]; - static thread_local std::vector main_hdr_ptrs; - main_hdr_ptrs.clear(); - main_hdr_ptrs.reserve(main_t.cols); - for (int c = 0; c < main_t.cols; ++c) main_hdr_ptrs.push_back(main_t.headers[c].c_str()); - const char* const* headers_in = main_hdr_ptrs.data(); - int col_count = main_t.cols; - const char* const* cells_in = main_t.cells; - int row_count_in = main_t.rows; - const ColumnType* declared_types_in = main_t.types.data(); - - // Joinables = todas las demas tablas. - static thread_local std::vector joinables_v; - joinables_v.clear(); - for (int i = 0; i < (int)tables.size(); ++i) { - if (i != main_idx) joinables_v.push_back(tables[(size_t)i]); - } - const std::vector* joinables = joinables_v.empty() ? nullptr : &joinables_v; - - auto& U_chrome = ui(); - bool chrome_visible = U_chrome.chrome_user_set ? U_chrome.chrome_user_visible : show_chrome; - - // Toggle Hide/Show UI siempre visible (botoncito arriba a la derecha). - { - float right = ImGui::GetWindowContentRegionMax().x; - ImGui::SetCursorPosX(right - 90.0f); - if (ImGui::SmallButton(chrome_visible ? "Hide UI##chrome" : "Show UI##chrome")) { - U_chrome.chrome_user_set = true; - U_chrome.chrome_user_visible = !chrome_visible; - } - } - - // Main source dropdown — solo si > 1 tabla disponibles. - if (chrome_visible && tables.size() > 1) { - ImGui::SameLine(); - float right = ImGui::GetWindowContentRegionMax().x; - ImGui::SetCursorPosX(right - 90.0f - 280.0f); - ImGui::TextDisabled("Main table:"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(180); - const char* cur_main = main_t.name.c_str(); - if (ImGui::BeginCombo("##main_table", cur_main)) { - for (const auto& t : tables) { - bool sel = (t.name == cur_main); - if (ImGui::Selectable(t.name.c_str(), sel)) { - st.main_source = t.name; - } - } - ImGui::EndCombo(); - } - } - - st.ensure_stage0(); - - // -------- Pre-pipeline: materialize joins -------- - // Si state.joins no vacio + joinables provistos, ejecutar chain de join_tables. - // El resultado reemplaza headers/cells/declared_types para el resto del render. - static thread_local std::vector joined_headers_store; - static thread_local std::vector joined_types_store; - static thread_local std::vector joined_headers_ptrs; - static thread_local std::vector joined_cells_ptrs; - static thread_local std::vector joined_declared_types; - static thread_local StageOutput joined_so; - - const char* const* headers = headers_in; - const char* const* cells = cells_in; - int row_count = row_count_in; - int orig_cols = col_count; - const ColumnType* declared_types = declared_types_in; - - bool joined = false; - if (!st.joins.empty() && joinables && !joinables->empty()) { - joined_so = StageOutput{}; - // Build initial left from main. - std::vector cur_h(orig_cols); - std::vector cur_t(orig_cols); - for (int c = 0; c < orig_cols; ++c) { - cur_h[c] = headers_in[c]; - cur_t[c] = declared_types_in ? declared_types_in[c] : ColumnType::Auto; - } - const char* const* cur_cells = cells_in; - int cur_rows = row_count_in; - int cur_cols = orig_cols; - - // Chain join por cada joins[i]. - std::vector chain; - chain.reserve(st.joins.size()); - for (const auto& jn : st.joins) { - const TableInput* match = nullptr; - for (const auto& ti : *joinables) { - if (ti.name == jn.source) { match = &ti; break; } - } - if (!match) continue; - StageOutput so = join_tables(cur_cells, cur_rows, cur_cols, - cur_h, cur_t, *match, jn); - chain.push_back(std::move(so)); - const StageOutput& last = chain.back(); - cur_cells = last.cells.data(); - cur_rows = last.rows; - cur_cols = last.cols; - cur_h = last.headers; - cur_t = last.types; - } - - if (!chain.empty()) { - joined = true; - joined_so = std::move(chain.back()); - joined_headers_store = joined_so.headers; - joined_types_store = joined_so.types; - joined_headers_ptrs.clear(); - joined_cells_ptrs.clear(); - for (const auto& s : joined_headers_store) joined_headers_ptrs.push_back(s.c_str()); - for (const auto& s : joined_so.cell_backing) joined_cells_ptrs.push_back(s.c_str()); - joined_declared_types = joined_types_store; - - headers = joined_headers_ptrs.data(); - cells = joined_cells_ptrs.data(); - row_count = joined_so.rows; - orig_cols = joined_so.cols; - declared_types = joined_declared_types.data(); - } - } - - Stage& stage0 = st.stages[0]; - int eff_cols = orig_cols + (int)stage0.derived.size(); - - ensure_init(st, eff_cols); - auto& U = ui(); - - // Build eff_headers / src_for_eff / eff_types para STAGE 0. - std::vector eff_headers(eff_cols); - std::vector src_for_eff(eff_cols); - std::vector eff_types(eff_cols); - for (int c = 0; c < eff_cols; ++c) { - if (c < orig_cols) { - eff_headers[c] = headers[c]; - src_for_eff[c] = c; - ColumnType d = declared_types ? declared_types[c] : ColumnType::Auto; - eff_types[c] = effective_type(d, cells, row_count, orig_cols, c); - } else { - const DerivedColumn& d = stage0.derived[c - orig_cols]; - eff_headers[c] = d.name.c_str(); - src_for_eff[c] = d.source_col; - eff_types[c] = d.type; - } - } - - static thread_local std::vector hn_storage; - static thread_local std::unordered_map name_to_col; - static thread_local std::unordered_map derived_n2i; - hn_storage.clear(); - name_to_col.clear(); - derived_n2i.clear(); - hn_storage.reserve(orig_cols); - for (int c = 0; c < orig_cols; ++c) { - hn_storage.emplace_back(headers[c]); - name_to_col[hn_storage.back()] = c; - } - for (int i = 0; i < (int)stage0.derived.size(); ++i) { - derived_n2i[stage0.derived[i].name] = i; - } - - // Re-fit auto en cambio de display, stage o config. - auto hash_cfg = [](const ViewConfig& c) -> size_t { - std::string s = c.x_col + "|" + c.cat_col + "|" + c.size_col; - for (auto& y : c.y_cols) { s += "|"; s += y; } - s += "|"; s += std::to_string(c.primary_color); - s += "|"; s += std::to_string(c.hist_bins); - s += "|"; s += std::to_string(c.pie_radius); - s += "|"; s += c.show_legend ? "1" : "0"; - s += "|"; s += c.show_markers ? "1" : "0"; - return std::hash{}(s); - }; - size_t cur_cfg_h = hash_cfg(st.viz_config); - if (U.prev_viz_display != st.display || U.prev_viz_stage != st.active_stage || - U.prev_viz_cfg_h != cur_cfg_h) { - st.viz_config.fit_request = true; - U.prev_viz_display = st.display; - U.prev_viz_stage = st.active_stage; - U.prev_viz_cfg_h = cur_cfg_h; - } - - // ----- Breadcrumb + viz selector (chrome) ----- - if (chrome_visible) { - draw_stage_breadcrumb(st); - draw_viz_selector(st); - } - int active = st.active_stage; - bool is_raw = (active == 0); - - // ----- Chips del stage activo ----- - Stage& act = st.stages[active]; - - if (is_raw && chrome_visible) { - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 2)); - // Joins chip row — solo si hay joinables disponibles. - if (joinables && !joinables->empty()) { - std::vector mh(orig_cols); - for (int c = 0; c < orig_cols; ++c) mh[c] = headers[c]; - draw_joins_chips(st, *joinables, mh); - } - - draw_filter_chips(act, eff_headers.data(), eff_cols, eff_types); - draw_add_filter_popup(act, eff_headers.data(), eff_cols, eff_types); - draw_edit_filter_popup(act, eff_headers.data(), eff_cols, eff_types); - - // Custom columns chips (solo stage 0) - { - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(110, 110, 110, 200)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(140, 140, 140, 230)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 85, 85, 85, 230)); - if (ImGui::SmallButton("+##addcustomcol")) { - U.cf_open = true; - U.cf_editing = false; - U.cf_edit_idx = -1; - U.cf_target_stage = 0; - U.cf_formula.clear(); - U.cf_name.clear(); - U.cf_type = ColumnType::String; - U.cf_error.clear(); - } - ImGui::PopStyleColor(3); - ImGui::SameLine(); - - bool any = false; - for (size_t i = 0; i < stage0.derived.size(); ++i) { - if (stage0.derived[i].formula.empty()) continue; - any = true; - const auto& d = stage0.derived[i]; - char buf[256]; - std::snprintf(buf, sizeof(buf), "%s %s x##custom%zu", - column_type_icon(d.type), d.name.c_str(), i); - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(140, 140, 140, 220)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(170, 170, 170, 240)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(110, 110, 110, 240)); - bool clicked = ImGui::SmallButton(buf); - ImGui::PopStyleColor(3); - if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - U.cf_open = true; - U.cf_editing = true; - U.cf_edit_idx = (int)i; - U.cf_target_stage = 0; - U.cf_formula = d.formula; - U.cf_name = d.name; - U.cf_type = d.type; - U.cf_error.clear(); - } - if (clicked) { - if (d.lua_id >= 0) lua_engine::release(lua_engine::get(), d.lua_id); - stage0.derived.erase(stage0.derived.begin() + i); - break; - } - ImGui::SameLine(); - } - if (!any) ImGui::TextDisabled("Custom columns: + para anadir."); - else ImGui::NewLine(); - } - - // Sort chips para stage 0 (input headers para popup). - draw_sort_chips(act); - draw_add_sort_popup(act, eff_headers.data(), eff_cols, eff_types); - draw_edit_sort_popup(act, eff_headers.data(), eff_cols); - ImGui::PopStyleVar(); // ItemSpacing - } - - // Para stages 1+, compute input headers/types del stage previo. - // Esto requiere compute_stage chain. Lo haremos abajo. - - // ---------- Compute view: chain compute_stage 0..active ---------- - // Stage 0 expressions: derived cols. Pero compute_stage no sabe de Lua. - // Estrategia: stage 0 lo aplicamos a mano (orig cells + filter + sort) - // y exponemos un eff_cells "virtual" donde derived cols se llenan via Lua - // en el render. Esto preserva el path actual. - // - // Para stages 1+, compute_stage opera sobre cells materializadas. Hay que - // materializar el stage 0 output como cells reales (con derived evaluadas). - - // Simpler: si active == 0, mantener el path actual (orig cells + Lua). - // Si active > 0, materializar stage 0 + chain compute_stage(stage 1..active). - - if (is_raw) { - // ----- Path stage 0: orig cells + filters/sort manuales + Lua per cell. - - // compute_visible_rows opera sobre orig cells. filter.col es eff col, - // hay que traducir a src col (igual que codigo anterior). - State st_tmp = st; - st_tmp.ensure_stage0(); - for (auto& f : st_tmp.stages[0].filters) { - if (f.col >= 0 && f.col < eff_cols) f.col = src_for_eff[f.col]; - } - // Sort: la pasamos por @idx convention. - st_tmp.stages[0].sorts.clear(); - if (!stage0.sorts.empty()) { - // resolve col name -> col idx (de eff_cols) -> src - const SortClause& sc0 = stage0.sorts.front(); - int sc_eff = -1; - for (int c = 0; c < eff_cols; ++c) { - if (std::strcmp(eff_headers[c], sc0.col.c_str()) == 0) { sc_eff = c; break; } - } - if (sc_eff >= 0) { - int sc_src = src_for_eff[sc_eff]; - char tmp[16]; std::snprintf(tmp, sizeof(tmp), "@%d", sc_src); - st_tmp.stages[0].sorts.push_back({tmp, sc0.desc}); - } - } - auto visible_rows = compute_visible_rows(cells, row_count, orig_cols, st_tmp); - - int visible_cols = 0; - for (int k = 0; k < eff_cols; ++k) if (st.col_visible[k]) ++visible_cols; - - // Snapshot del active output (stage 0) para el config popup. - U.active_headers.clear(); - U.active_types.clear(); - for (int k = 0; k < eff_cols; ++k) { - if (!st.col_visible[k]) continue; - U.active_headers.emplace_back(eff_headers[k]); - U.active_types.push_back(eff_types[k]); - } - // Input == orig + derived (stage 0 no tiene upstream que agrupe). - U.input_headers_active = U.active_headers; - U.input_types_active = U.active_types; - - if (chrome_visible) - { - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 2)); - ImGui::Text("Filas: %d / %d Columnas: %d / %d", - (int)visible_rows.size(), row_count, visible_cols, eff_cols); - ImGui::SameLine(); - if (ImGui::SmallButton(U.stats_mode ? "Hide stats" : "Show stats")) { - U.stats_mode = !U.stats_mode; - } - ImGui::SameLine(); - if (ImGui::SmallButton("Export CSV")) { - std::string out; - bool first = true; - for (int oc = 0; oc < (int)st.col_order.size(); ++oc) { - int c = st.col_order[oc]; - if (c < 0 || c >= eff_cols) continue; - if (!st.col_visible[c]) continue; - if (!first) out += ','; - out += csv_escape(eff_headers[c]); - first = false; - } - out += '\n'; - for (int r : visible_rows) { - first = true; - for (int oc = 0; oc < (int)st.col_order.size(); ++oc) { - int c = st.col_order[oc]; - if (c < 0 || c >= eff_cols) continue; - if (!st.col_visible[c]) continue; - int src = src_for_eff[c]; - if (!first) out += ','; - out += csv_escape(cells[r * orig_cols + src]); - first = false; - } - out += '\n'; - } - const char* p = fn::local_path("export_table.csv"); - std::ofstream f(p, std::ios::binary | std::ios::trunc); - if (f) { f << out; U.last_export_path = p; } - } - if (!U.last_export_path.empty()) { - ImGui::SameLine(); - ImGui::TextDisabled("-> %s", U.last_export_path.c_str()); - } - ImGui::SameLine(); - if (ImGui::SmallButton("Show TQL")) { - std::vector orig_headers(orig_cols); - std::vector orig_types(orig_cols); - for (int c = 0; c < orig_cols; ++c) { - orig_headers[c] = headers[c]; - orig_types[c] = eff_types[c]; - } - U.tql_show_text = tql::emit(st, orig_headers, orig_types); - U.tql_show_open = true; - } - ImGui::SameLine(); - if (ImGui::SmallButton("Apply TQL")) { - U.tql_apply_open = true; - U.tql_apply_error.clear(); - } - ImGui::SameLine(); - ImGui::SetNextItemWidth(160); - ImGui::InputText("##tql_file", U.tql_file_path, sizeof(U.tql_file_path)); - ImGui::SameLine(); - if (ImGui::SmallButton("Save .tql")) { - std::vector oh(orig_cols); - std::vector ot(orig_cols); - for (int c = 0; c < orig_cols; ++c) { oh[c] = headers[c]; ot[c] = eff_types[c]; } - std::string text = tql::emit(st, oh, ot); - const char* path = fn::local_path(U.tql_file_path); - std::ofstream f(path); - if (f) { f << text; U.tql_io_status = std::string("saved: ") + path; } - else { U.tql_io_status = std::string("save FAILED: ") + path; } - } - ImGui::SameLine(); - if (ImGui::SmallButton("Load .tql")) { - const char* path = fn::local_path(U.tql_file_path); - std::ifstream f(path); - if (!f) { U.tql_io_status = std::string("load FAILED: ") + path; } - else { - std::string text((std::istreambuf_iterator(f)), - std::istreambuf_iterator()); - std::vector oh(orig_cols); - std::vector ot(orig_cols); - for (int c = 0; c < orig_cols; ++c) { oh[c] = headers[c]; ot[c] = eff_types[c]; } - std::string err; - bool ok = tql::apply(text, st, oh, ot, cells, row_count, orig_cols, &err); - if (ok) U.tql_io_status = std::string("loaded: ") + path + (err.empty() ? "" : " (warn: " + err + ")"); - else U.tql_io_status = std::string("load parse error: ") + err; - } - } - if (!U.tql_io_status.empty()) { - ImGui::SameLine(); - ImGui::TextDisabled("%s", U.tql_io_status.c_str()); - } - ImGui::PopStyleVar(); - } // chrome_visible - maybe_recompute_stats(cells, row_count, orig_cols, eff_cols, - st_tmp.stages[0].filters, - visible_rows, src_for_eff); - - // Toggle Table <-> View (siempre visible) - draw_table_toggle(st.display, U.last_non_table_main, "main", &st); - - // SO compartido: main viz + extras. Construido on-demand. - StageOutput so_main; - bool so_built = false; - auto build_so = [&]() -> StageOutput& { - if (so_built) return so_main; - so_built = true; - std::vector vcols; - for (int c = 0; c < eff_cols; ++c) if (st.col_visible[c]) vcols.push_back(c); - so_main.cols = (int)vcols.size(); - so_main.rows = (int)visible_rows.size(); - so_main.headers.reserve(so_main.cols); - so_main.types.reserve(so_main.cols); - for (int c : vcols) { - so_main.headers.emplace_back(eff_headers[c]); - so_main.types.push_back(eff_types[c]); - } - so_main.cell_backing.reserve((size_t)so_main.rows * so_main.cols); - for (int r : visible_rows) { - for (int c : vcols) { - if (c < orig_cols) { - const char* p = cells[r * orig_cols + c]; - so_main.cell_backing.emplace_back(p ? p : ""); - } else { - const DerivedColumn& d = stage0.derived[c - orig_cols]; - if (!d.formula.empty() && d.lua_id >= 0) { - lua_engine::RowCtx ctx; - ctx.cells = cells; - ctx.orig_cols = orig_cols; - ctx.row = r; - ctx.header_names = &hn_storage; - ctx.name_to_col = &name_to_col; - ctx.types_orig = eff_types.data(); - ctx.n_types_orig = orig_cols; - ctx.derived = &stage0.derived; - ctx.derived_name_to_idx = &derived_n2i; - std::string err; - so_main.cell_backing.emplace_back( - lua_engine::eval(lua_engine::get(), d.lua_id, ctx, &err)); - } else { - int src = d.source_col; - const char* sp = (src >= 0 && src < orig_cols) ? cells[r * orig_cols + src] : ""; - so_main.cell_backing.emplace_back(sp ? sp : ""); - } - } - } - } - so_main.cells.reserve(so_main.cell_backing.size()); - for (auto& s : so_main.cell_backing) so_main.cells.push_back(s.c_str()); - return so_main; - }; - - if (visible_cols == 0) { - ImGui::TextDisabled("(todas las columnas ocultas)"); - // Modales fuera del table block. - } else if (st.display != ViewMode::Table) { - viz::render(build_so(), st.display, st.viz_config, ImVec2(-1, -1)); - } else { - ImGuiTableFlags flags = - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | - ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; - if (ImGui::BeginTable(id, visible_cols, flags, ImVec2(0, 0))) { - - for (int dc = 0; dc < (int)st.col_order.size(); ++dc) { - int c = st.col_order[dc]; - if (c < 0 || c >= eff_cols) continue; - if (!st.col_visible[c]) continue; - ImGui::TableSetupColumn(eff_headers[c], ImGuiTableColumnFlags_None, 0.0f, (ImGuiID)c); - } - ImGui::TableSetupScrollFreeze(0, 1); - - ImGui::TableNextRow(ImGuiTableRowFlags_Headers); - for (int dc = 0; dc < (int)st.col_order.size(); ++dc) { - int c = st.col_order[dc]; - if (c < 0 || c >= eff_cols) continue; - if (!st.col_visible[c]) continue; - ImGui::TableSetColumnIndex(dc); // visual idx aprox; recomputado por engine - ImGui::PushID(c); - - // Detecta si esta col esta en sorts (primario o secundario) - int sort_pos = -1; - bool sort_desc = false; - for (size_t si = 0; si < act.sorts.size(); ++si) { - if (act.sorts[si].col == eff_headers[c]) { - sort_pos = (int)si; sort_desc = act.sorts[si].desc; break; - } - } - char arrow[16] = ""; - if (sort_pos == 0) std::snprintf(arrow, sizeof(arrow), " %s", sort_desc ? "v" : "^"); - else if (sort_pos > 0) std::snprintf(arrow, sizeof(arrow), " %s%d", sort_desc ? "v" : "^", sort_pos + 1); - char label[200]; - std::snprintf(label, sizeof(label), "%s %s%s", - column_type_icon(eff_types[c]), eff_headers[c], arrow); - - ImGui::PushStyleColor(ImGuiCol_Header, IM_COL32(45, 50, 65, 200)); - ImGui::PushStyleColor(ImGuiCol_HeaderHovered, IM_COL32(65, 75, 95, 220)); - ImGui::PushStyleColor(ImGuiCol_HeaderActive, IM_COL32(80, 95, 130, 240)); - bool clicked = ImGui::Selectable(label, false, ImGuiSelectableFlags_DontClosePopups); - ImGui::PopStyleColor(3); - - if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) { - ImGui::SetDragDropPayload("##colreorder", &c, sizeof(int)); - ImGui::Text("Move %s", eff_headers[c]); - ImGui::EndDragDropSource(); - } - if (ImGui::BeginDragDropTarget()) { - if (const ImGuiPayload* p = ImGui::AcceptDragDropPayload("##colreorder")) { - int src = *(const int*)p->Data; - reorder_column(st, src, c); - } - ImGui::EndDragDropTarget(); - } - if (clicked) { - bool shift = ImGui::GetIO().KeyShift; - apply_header_sort_click(act, eff_headers[c], shift); - } - if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - U.header_popup_col = c; - ImGui::OpenPopup("##hdr_menu"); - } - if (ImGui::BeginPopup("##hdr_menu") && U.header_popup_col == c) { - draw_header_menu(st, act, c, eff_headers.data(), eff_cols, eff_types, orig_cols, true); - ImGui::EndPopup(); - } - - if (U.stats_mode && c < (int)U.stats_cache.size()) { - const ColStats& s = U.stats_cache[c]; - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(170, 190, 220, 220)); - ImGui::Text("missing: %d", s.empty_count); - ImGui::Text("uniq: %d%s", s.unique_count, s.unique_capped ? "+" : ""); - if (s.numeric) { - ImGui::Text("mean: %.2f", s.mean); - ImGui::Text("p25: %.2f", s.p25); - ImGui::Text("p50: %.2f", s.p50); - ImGui::Text("p75: %.2f", s.p75); - if (!s.hist.empty()) { - char overlay[64]; - std::snprintf(overlay, sizeof(overlay), "[%.2f..%.2f]", s.min, s.max); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(70, 140, 220, 230)); - ImGui::PlotHistogram("##hist", s.hist.data(), (int)s.hist.size(), - 0, overlay, 0.0f, FLT_MAX, ImVec2(-1, 36)); - ImGui::PopStyleColor(); - } - } else if (!s.top_categories.empty()) { - int mx = 0; - for (const auto& kv : s.top_categories) if (kv.second > mx) mx = kv.second; - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(70, 140, 220, 220)); - for (const auto& kv : s.top_categories) { - float frac = mx > 0 ? (float)kv.second / (float)mx : 0.f; - char ovl[96]; - std::snprintf(ovl, sizeof(ovl), "%s (%d)", kv.first.c_str(), kv.second); - ImGui::ProgressBar(frac, ImVec2(-1, 12), ovl); - } - ImGui::PopStyleColor(); - } - ImGui::PopStyleColor(); - } - ImGui::PopID(); - } - - int sel_rmin = std::min(U.sel_anchor_row, U.sel_end_row); - int sel_rmax = std::max(U.sel_anchor_row, U.sel_end_row); - int sel_cmin = std::min(U.sel_anchor_col, U.sel_end_col); - int sel_cmax = std::max(U.sel_anchor_col, U.sel_end_col); - - ImGuiListClipper clipper; - clipper.Begin((int)visible_rows.size()); - while (clipper.Step()) { - for (int ri = clipper.DisplayStart; ri < clipper.DisplayEnd; ++ri) { - int r = visible_rows[ri]; - ImGui::TableNextRow(); - int draw_idx = 0; - for (int oc = 0; oc < (int)st.col_order.size(); ++oc) { - int c = st.col_order[oc]; - if (c < 0 || c >= eff_cols) continue; - if (!st.col_visible[c]) continue; - ImGui::TableSetColumnIndex(draw_idx++); - int src = src_for_eff[c]; - std::string eval_buf; - const char* cell; - if (c >= orig_cols && !stage0.derived[c - orig_cols].formula.empty()) { - const auto& d = stage0.derived[c - orig_cols]; - if (d.lua_id < 0) cell = "?"; - else { - lua_engine::RowCtx ctx; - ctx.cells = cells; - ctx.orig_cols = orig_cols; - ctx.row = r; - ctx.header_names = &hn_storage; - ctx.name_to_col = &name_to_col; - ctx.types_orig = eff_types.data(); - ctx.n_types_orig = orig_cols; - ctx.derived = &stage0.derived; - ctx.derived_name_to_idx = &derived_n2i; - std::string err; - eval_buf = lua_engine::eval(lua_engine::get(), d.lua_id, ctx, &err); - cell = eval_buf.c_str(); - } - } else { - cell = cells[r * orig_cols + src]; - } - - for (const auto& cr : st.color_rules) { - if (cr.col == c && cell && cr.equals == cell) { - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, (ImU32)cr.color); - break; - } - } - bool in_sel = (U.sel_active && - ri >= sel_rmin && ri <= sel_rmax && - oc >= sel_cmin && oc <= sel_cmax); - ImGui::PushID(r * eff_cols + c); - ImGui::Selectable(cell ? cell : "", in_sel, - ImGuiSelectableFlags_AllowDoubleClick); - // AllowWhenBlockedByActiveItem: durante drag, - // otras celdas tambien reciben hover -> sel se - // pinta mientras arrastras. - bool hovered = ImGui::IsItemHovered( - ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); - if (hovered) { - if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { - U.sel_anchor_row = ri; U.sel_anchor_col = oc; - U.sel_end_row = ri; U.sel_end_col = oc; - U.sel_active = true; - U.sel_dragging = true; - } else if (U.sel_dragging) { - U.sel_end_row = ri; U.sel_end_col = oc; - } - if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { - U.pending_col = c; - U.pending_value = cell ? cell : ""; - U.open_cell_popup = true; - } - } - ImGui::PopID(); - } - } - } - if (U.sel_dragging && !ImGui::IsMouseDown(ImGuiMouseButton_Left)) { - U.sel_dragging = false; - } - ImGui::EndTable(); - } - - // Ctrl+C -> TSV. - if (U.sel_active && ImGui::GetIO().KeyCtrl && - ImGui::IsKeyPressed(ImGuiKey_C, false)) - { - int rmin = std::min(U.sel_anchor_row, U.sel_end_row); - int rmax = std::max(U.sel_anchor_row, U.sel_end_row); - int cmin = std::min(U.sel_anchor_col, U.sel_end_col); - int cmax = std::max(U.sel_anchor_col, U.sel_end_col); - std::string out; - bool first = true; - for (int oc = cmin; oc <= cmax; ++oc) { - if (oc < 0 || oc >= (int)st.col_order.size()) continue; - int c = st.col_order[oc]; - if (c < 0 || c >= eff_cols) continue; - if (!st.col_visible[c]) continue; - if (!first) out += '\t'; - out += eff_headers[c]; - first = false; - } - out += '\n'; - for (int ri = rmin; ri <= rmax; ++ri) { - if (ri < 0 || ri >= (int)visible_rows.size()) continue; - int r = visible_rows[ri]; - first = true; - for (int oc = cmin; oc <= cmax; ++oc) { - if (oc < 0 || oc >= (int)st.col_order.size()) continue; - int c = st.col_order[oc]; - if (c < 0 || c >= eff_cols) continue; - if (!st.col_visible[c]) continue; - int src = src_for_eff[c]; - const char* v = cells[r * orig_cols + src]; - std::string sv = v ? v : ""; - for (char& ch : sv) if (ch == '\t' || ch == '\n') ch = ' '; - if (!first) out += '\t'; - out += sv; - first = false; - } - out += '\n'; - } - ImGui::SetClipboardText(out.c_str()); - } - } - - // Render extras panels (stage 0 path) - if (!st.extra_panels.empty() && visible_cols > 0) { - int close_idx = -1; - for (int i = 0; i < (int)st.extra_panels.size(); ++i) { - if (draw_extra_panel(st, st.extra_panels[i], i, build_so())) close_idx = i; - } - if (close_idx >= 0) st.extra_panels.erase(st.extra_panels.begin() + close_idx); - } - } else { - // ----- Path stage > 0: materializar stage 0 con cells reales + chain. - // Materializar stage 0: aplicar filters/sort sobre orig + evaluar derived. - State st_tmp = st; - for (auto& f : st_tmp.stages[0].filters) { - if (f.col >= 0 && f.col < eff_cols) f.col = src_for_eff[f.col]; - } - st_tmp.stages[0].sorts.clear(); - if (!stage0.sorts.empty()) { - const SortClause& sc0 = stage0.sorts.front(); - int sc_eff = -1; - for (int c = 0; c < eff_cols; ++c) { - if (std::strcmp(eff_headers[c], sc0.col.c_str()) == 0) { sc_eff = c; break; } - } - if (sc_eff >= 0) { - int sc_src = src_for_eff[sc_eff]; - char tmp[16]; std::snprintf(tmp, sizeof(tmp), "@%d", sc_src); - st_tmp.stages[0].sorts.push_back({tmp, sc0.desc}); - } - } - auto vrows = compute_visible_rows(cells, row_count, orig_cols, st_tmp); - - // Materializar stage0 output: cells (eff_cols) con derived evaluadas. - std::vector mat_backing; - std::vector mat_cells; - mat_backing.reserve((size_t)vrows.size() * eff_cols); - mat_cells.reserve((size_t)vrows.size() * eff_cols); - - for (int r : vrows) { - for (int c = 0; c < eff_cols; ++c) { - const char* p; - std::string buf; - if (c < orig_cols) { - p = cells[r * orig_cols + c]; - mat_backing.emplace_back(p ? p : ""); - } else { - const DerivedColumn& d = stage0.derived[c - orig_cols]; - if (!d.formula.empty()) { - if (d.lua_id < 0) { - mat_backing.emplace_back(""); - } else { - lua_engine::RowCtx ctx; - ctx.cells = cells; - ctx.orig_cols = orig_cols; - ctx.row = r; - ctx.header_names = &hn_storage; - ctx.name_to_col = &name_to_col; - ctx.types_orig = eff_types.data(); - ctx.n_types_orig = orig_cols; - ctx.derived = &stage0.derived; - ctx.derived_name_to_idx = &derived_n2i; - std::string err; - mat_backing.emplace_back( - lua_engine::eval(lua_engine::get(), d.lua_id, ctx, &err)); - } - } else { - // retipo puro - int src = d.source_col; - const char* sp = (src >= 0 && src < orig_cols) ? cells[r * orig_cols + src] : ""; - mat_backing.emplace_back(sp ? sp : ""); - } - } - } - } - // Punteros tras llenar backing (reserve garantiza no realloc). - for (auto& s : mat_backing) mat_cells.push_back(s.c_str()); - - std::vector cur_headers(eff_cols); - std::vector cur_types(eff_cols); - for (int c = 0; c < eff_cols; ++c) { - cur_headers[c] = eff_headers[c]; - cur_types[c] = eff_types[c]; - } - - // Chain compute_stage 1..active. - // Para encadenar, mantenemos vectores por iteracion. cur_cells apunta al - // ultimo output. - const char* const* cur_cells = mat_cells.data(); - int cur_rows = (int)vrows.size(); - int cur_cols_n = eff_cols; - - std::vector outs; - outs.reserve(st.stages.size()); - - // Headers del INPUT del active (= output del active-1) - std::vector input_headers_active = cur_headers; - std::vector input_types_active = cur_types; - - for (int si = 1; si <= active; ++si) { - const Stage& sN = st.stages[si]; - // Antes de computar: si es el active stage, los input_headers son cur_*. - if (si == active) { - input_headers_active = cur_headers; - input_types_active = cur_types; - } - StageOutput so = compute_stage(cur_cells, cur_rows, cur_cols_n, - cur_headers, cur_types, sN); - outs.push_back(std::move(so)); - const StageOutput& last = outs.back(); - cur_cells = last.cells.data(); - cur_rows = last.rows; - cur_cols_n = last.cols; - cur_headers = last.headers; - cur_types = last.types; - } - - // ----- Chips del active stage (uses input_headers_active) ----- - std::vector ih_ptrs(input_headers_active.size()); - for (size_t i = 0; i < input_headers_active.size(); ++i) - ih_ptrs[i] = input_headers_active[i].c_str(); - int in_cols_n = (int)input_headers_active.size(); - - if (chrome_visible) { - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 2)); - draw_filter_chips(act, ih_ptrs.data(), in_cols_n, input_types_active); - draw_add_filter_popup(act, ih_ptrs.data(), in_cols_n, input_types_active); - draw_edit_filter_popup(act, ih_ptrs.data(), in_cols_n, input_types_active); - - draw_breakout_chips(act, ih_ptrs.data(), in_cols_n, input_types_active); - draw_add_breakout_popup(act, ih_ptrs.data(), in_cols_n, input_types_active, - cur_cells, cur_rows); - draw_edit_breakout_popup(act, ih_ptrs.data(), in_cols_n); - - draw_aggregation_chips(act, ih_ptrs.data(), in_cols_n); - draw_add_aggregation_popup(act, ih_ptrs.data(), in_cols_n, input_types_active); - draw_edit_agg_popup(act, ih_ptrs.data(), in_cols_n); - - // ----- Custom column chips (stages 1+, target = active stage) ----- - { - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(110, 110, 110, 200)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(140, 140, 140, 230)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 85, 85, 85, 230)); - if (ImGui::SmallButton("+##addcustomcol_stage")) { - U.cf_open = true; - U.cf_editing = false; - U.cf_edit_idx = -1; - U.cf_target_stage = active; - U.cf_formula.clear(); - U.cf_name.clear(); - U.cf_type = ColumnType::String; - U.cf_error.clear(); - } - ImGui::PopStyleColor(3); - ImGui::SameLine(); - bool any = false; - for (size_t i = 0; i < act.derived.size(); ++i) { - if (act.derived[i].formula.empty()) continue; - any = true; - const auto& d = act.derived[i]; - char buf[256]; - std::snprintf(buf, sizeof(buf), "%s %s x##custom_st_%zu", - column_type_icon(d.type), d.name.c_str(), i); - ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(140, 140, 140, 220)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(170, 170, 170, 240)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(110, 110, 110, 240)); - bool clicked = ImGui::SmallButton(buf); - ImGui::PopStyleColor(3); - if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - U.cf_open = true; - U.cf_editing = true; - U.cf_edit_idx = (int)i; - U.cf_target_stage = active; - U.cf_formula = d.formula; - U.cf_name = d.name; - U.cf_type = d.type; - U.cf_error.clear(); - } - if (clicked) { - if (d.lua_id >= 0) lua_engine::release(lua_engine::get(), d.lua_id); - act.derived.erase(act.derived.begin() + i); - break; - } - ImGui::SameLine(); - } - if (!any) ImGui::TextDisabled("Custom columns (stage %d): + para anadir.", active); - else ImGui::NewLine(); - } - - draw_sort_chips(act); - // Sort col options son los headers del OUTPUT del stage activo. - std::vector out_h_ptrs(cur_headers.size()); - for (size_t i = 0; i < cur_headers.size(); ++i) out_h_ptrs[i] = cur_headers[i].c_str(); - draw_add_sort_popup(act, out_h_ptrs.data(), (int)cur_headers.size(), cur_types); - draw_edit_sort_popup(act, out_h_ptrs.data(), (int)cur_headers.size()); - ImGui::PopStyleVar(); - } // chrome_visible - - // ----- Materializar act.derived sobre cur_cells ----- - // Para cada derived col formula del active stage, eval per output row. - std::vector ext_backing; - std::vector ext_cells; - std::vector ext_headers; - std::vector ext_types; - if (!act.derived.empty()) { - int orig_out_cols = cur_cols_n; - std::vector out_hn = cur_headers; - std::unordered_map out_n2c; - for (size_t i = 0; i < out_hn.size(); ++i) out_n2c[out_hn[i]] = (int)i; - int n_derived = (int)act.derived.size(); - int new_cols = orig_out_cols + n_derived; - ext_backing.reserve((size_t)cur_rows * n_derived); - ext_cells.reserve((size_t)cur_rows * new_cols); - for (int r = 0; r < cur_rows; ++r) { - // copia cols originales del output - for (int c = 0; c < orig_out_cols; ++c) { - ext_cells.push_back(cur_cells[r * orig_out_cols + c]); - } - // anade derived eval - for (int k = 0; k < n_derived; ++k) { - const DerivedColumn& d = act.derived[k]; - if (d.formula.empty() || d.lua_id < 0) { - ext_backing.emplace_back(""); - } else { - lua_engine::RowCtx ctx; - ctx.cells = cur_cells; - ctx.orig_cols = orig_out_cols; - ctx.row = r; - ctx.header_names = &out_hn; - ctx.name_to_col = &out_n2c; - ctx.types_orig = cur_types.data(); - ctx.n_types_orig = orig_out_cols; - std::string e; - ext_backing.emplace_back( - lua_engine::eval(lua_engine::get(), d.lua_id, ctx, &e)); - } - // marker placeholder; sera replaced abajo tras backing estable - ext_cells.push_back(nullptr); - } - } - // Construir ext_cells reemplazando placeholders por punteros estables. - size_t bi = 0; - for (int r = 0; r < cur_rows; ++r) { - for (int k = 0; k < n_derived; ++k) { - int idx = r * new_cols + orig_out_cols + k; - ext_cells[idx] = ext_backing[bi++].c_str(); - } - } - ext_headers = cur_headers; - ext_types = cur_types; - for (int k = 0; k < n_derived; ++k) { - ext_headers.push_back(act.derived[k].name); - ext_types.push_back(act.derived[k].type); - } - cur_cells = ext_cells.data(); - cur_cols_n = new_cols; - cur_headers = ext_headers; - cur_types = ext_types; - } - - // Header row + cells render simple (sin clipper porque outputs son - // pequenos tipicamente). - // Snapshot del active output (stage>0) para config popup. - U.active_headers = cur_headers; - U.active_types = cur_types; - // Input del active stage = output del previo. Disponible en - // input_headers_active/input_types_active. - U.input_headers_active = input_headers_active; - U.input_types_active = input_types_active; - - if (chrome_visible) { - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 2)); - ImGui::Text("Filas: %d Columnas: %d", cur_rows, cur_cols_n); - ImGui::SameLine(); - if (ImGui::SmallButton(U.stats_mode ? "Hide stats" : "Show stats")) { - U.stats_mode = !U.stats_mode; - } - // Recompute stats sobre cur_cells del stage activo. - if (U.stats_mode && cur_cols_n > 0) { - U.stats_cache.resize(cur_cols_n); - U.last_cells = cur_cells; - for (int c = 0; c < cur_cols_n; ++c) { - U.stats_cache[c] = compute_column_stats(cur_cells, cur_rows, cur_cols_n, c); - } - } - ImGui::SameLine(); - if (ImGui::SmallButton("Show TQL")) { - std::vector oh(orig_cols); - std::vector ot(orig_cols); - for (int c = 0; c < orig_cols; ++c) { - oh[c] = headers[c]; - ot[c] = eff_types[c]; - } - U.tql_show_text = tql::emit(st, oh, ot); - U.tql_show_open = true; - } - ImGui::SameLine(); - if (ImGui::SmallButton("Apply TQL")) { - U.tql_apply_open = true; - U.tql_apply_error.clear(); - } - ImGui::SameLine(); - ImGui::SetNextItemWidth(160); - ImGui::InputText("##tql_file2", U.tql_file_path, sizeof(U.tql_file_path)); - ImGui::SameLine(); - if (ImGui::SmallButton("Save .tql##s2")) { - std::vector oh(orig_cols); - std::vector ot(orig_cols); - for (int c = 0; c < orig_cols; ++c) { oh[c] = headers[c]; ot[c] = eff_types[c]; } - std::string text = tql::emit(st, oh, ot); - const char* path = fn::local_path(U.tql_file_path); - std::ofstream f(path); - if (f) { f << text; U.tql_io_status = std::string("saved: ") + path; } - else { U.tql_io_status = std::string("save FAILED: ") + path; } - } - ImGui::SameLine(); - if (ImGui::SmallButton("Load .tql##l2")) { - const char* path = fn::local_path(U.tql_file_path); - std::ifstream f(path); - if (!f) { U.tql_io_status = std::string("load FAILED: ") + path; } - else { - std::string text((std::istreambuf_iterator(f)), - std::istreambuf_iterator()); - std::vector oh(orig_cols); - std::vector ot(orig_cols); - for (int c = 0; c < orig_cols; ++c) { oh[c] = headers[c]; ot[c] = eff_types[c]; } - std::string err; - bool ok = tql::apply(text, st, oh, ot, cells, row_count, orig_cols, &err); - if (ok) U.tql_io_status = std::string("loaded: ") + path + (err.empty() ? "" : " (warn: " + err + ")"); - else U.tql_io_status = std::string("load parse error: ") + err; - } - } - if (!U.tql_io_status.empty()) { - ImGui::SameLine(); - ImGui::TextDisabled("%s", U.tql_io_status.c_str()); - } - ImGui::PopStyleVar(); - } // chrome_visible - - // Toggle Table <-> View (siempre visible) - draw_table_toggle(st.display, U.last_non_table_main, "main2", &st); - - if (st.display != ViewMode::Table && cur_cols_n > 0) { - // outs.back() es el StageOutput del active. Si active no tiene outs - // (cur_rows poblado pero outs vacio cuando active>0 y chain corta), - // construir uno on-the-fly desde cur_cells. - StageOutput so_local; - const StageOutput* so_ptr = nullptr; - if (!outs.empty()) { - so_ptr = &outs.back(); - } else { - so_local.cols = cur_cols_n; - so_local.rows = cur_rows; - so_local.headers = cur_headers; - so_local.types = cur_types; - so_local.cells.reserve((size_t)cur_rows * cur_cols_n); - for (int i = 0; i < cur_rows * cur_cols_n; ++i) - so_local.cells.push_back(cur_cells[i]); - so_ptr = &so_local; - } - int clicked_row = -1; - viz::render(*so_ptr, st.display, st.viz_config, ImVec2(-1, -1), &clicked_row); - // Fase 10: click sobre chart -> drill al stage previo usando - // breakout col[0] como filtro Op::Eq sobre cells[clicked_row]. - if (clicked_row >= 0 && active > 0 && - so_ptr->cols > 0 && clicked_row < so_ptr->rows) { - int n_brk = (int)st.stages[active].breakouts.size(); - if (n_brk > 0) { - const char* v = so_ptr->cells[clicked_row * so_ptr->cols + 0]; - std::string col_clean; - parse_breakout_granularity(so_ptr->headers[0], col_clean); - drill_into(st, active, col_clean, - v ? std::string(v) : "", - input_headers_active); - } - } - goto stage_n_table_end; - } - - { - ImGuiTableFlags flags = - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | - ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; - if (cur_cols_n > 0 && ImGui::BeginTable(id, cur_cols_n, flags, ImVec2(0, 0))) { - for (int c = 0; c < cur_cols_n; ++c) { - ImGui::TableSetupColumn(cur_headers[c].c_str(), - ImGuiTableColumnFlags_None, 0.0f, (ImGuiID)c); - } - ImGui::TableSetupScrollFreeze(0, 1); - - // Custom header row: nombre + icon + stats inline si stats_mode. - ImGui::TableNextRow(ImGuiTableRowFlags_Headers); - for (int c = 0; c < cur_cols_n; ++c) { - ImGui::TableSetColumnIndex(c); - // Sort indicator - int sort_pos = -1; - bool sort_desc = false; - for (size_t si = 0; si < act.sorts.size(); ++si) { - if (act.sorts[si].col == cur_headers[c]) { - sort_pos = (int)si; sort_desc = act.sorts[si].desc; break; - } - } - char arrow[16] = ""; - if (sort_pos == 0) std::snprintf(arrow, sizeof(arrow), " %s", sort_desc ? "v" : "^"); - else if (sort_pos > 0) std::snprintf(arrow, sizeof(arrow), " %s%d", sort_desc ? "v" : "^", sort_pos + 1); - - ImGui::PushStyleColor(ImGuiCol_Header, IM_COL32(45, 50, 65, 200)); - ImGui::PushStyleColor(ImGuiCol_HeaderHovered, IM_COL32(65, 75, 95, 220)); - ImGui::PushStyleColor(ImGuiCol_HeaderActive, IM_COL32(80, 95, 130, 240)); - char lbl[200]; - std::snprintf(lbl, sizeof(lbl), "%s %s%s", - column_type_icon(cur_types[c]), - cur_headers[c].c_str(), arrow); - bool h_clicked = ImGui::Selectable(lbl, false, ImGuiSelectableFlags_DontClosePopups); - ImGui::PopStyleColor(3); - if (h_clicked) { - bool shift = ImGui::GetIO().KeyShift; - apply_header_sort_click(act, cur_headers[c], shift); - } - - if (U.stats_mode && c < (int)U.stats_cache.size()) { - const ColStats& s = U.stats_cache[c]; - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(170, 190, 220, 220)); - ImGui::Text("missing: %d", s.empty_count); - ImGui::Text("uniq: %d%s", s.unique_count, s.unique_capped ? "+" : ""); - if (s.numeric) { - ImGui::Text("mean: %.2f", s.mean); - ImGui::Text("p25: %.2f", s.p25); - ImGui::Text("p50: %.2f", s.p50); - ImGui::Text("p75: %.2f", s.p75); - if (!s.hist.empty()) { - char overlay[64]; - std::snprintf(overlay, sizeof(overlay), "[%.2f..%.2f]", s.min, s.max); - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(70, 140, 220, 230)); - ImGui::PlotHistogram("##hist", s.hist.data(), (int)s.hist.size(), - 0, overlay, 0.0f, FLT_MAX, ImVec2(-1, 36)); - ImGui::PopStyleColor(); - } - } else if (!s.top_categories.empty()) { - int mx = 0; - for (const auto& kv : s.top_categories) if (kv.second > mx) mx = kv.second; - ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(70, 140, 220, 220)); - for (const auto& kv : s.top_categories) { - float frac = mx > 0 ? (float)kv.second / (float)mx : 0.f; - char ovl[96]; - std::snprintf(ovl, sizeof(ovl), "%s (%d)", kv.first.c_str(), kv.second); - ImGui::ProgressBar(frac, ImVec2(-1, 12), ovl); - } - ImGui::PopStyleColor(); - } - ImGui::PopStyleColor(); - } - } - - int n_brk = (int)st.stages[active].breakouts.size(); - - for (int r = 0; r < cur_rows; ++r) { - ImGui::TableNextRow(); - for (int c = 0; c < cur_cols_n; ++c) { - ImGui::TableSetColumnIndex(c); - const char* cell = cur_cells[r * cur_cols_n + c]; - ImGui::PushID(r * cur_cols_n + c); - ImGui::Selectable(cell ? cell : ""); - if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { - U.pending_col = c; - U.pending_value = cell ? cell : ""; - U.inspect_row = r; - ImGui::OpenPopup("##drill_popup"); - } - if (ImGui::BeginPopup("##drill_popup")) { - if (c < n_brk) { - char lbl[256]; - std::snprintf(lbl, sizeof(lbl), "Drill into: %s = %s", - cur_headers[c].c_str(), cell ? cell : ""); - if (ImGui::MenuItem(lbl)) { - drill_into(st, active, cur_headers[c], - cell ? std::string(cell) : "", - input_headers_active); - ImGui::CloseCurrentPopup(); - } - ImGui::Separator(); - } - if (ImGui::MenuItem("Inspect row...")) { - U.inspect_row = r; - U.inspect_open = true; - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } - ImGui::PopID(); - } - } - ImGui::EndTable(); - } - } - stage_n_table_end:; - - // Row inspector modal (fase 10). Activado via right-click "Inspect row..." - // sobre celdas del table del stage activo. `cur_cells` ya es row-major. - draw_row_inspector_modal(st, active, cur_cells, cur_rows, cur_cols_n, - cur_headers, cur_types, input_headers_active); - - // Render extras (stage>0 path) - if (!st.extra_panels.empty() && cur_cols_n > 0) { - StageOutput so_local; - const StageOutput* so_ptr = nullptr; - if (!outs.empty()) { - so_ptr = &outs.back(); - } else { - so_local.cols = cur_cols_n; - so_local.rows = cur_rows; - so_local.headers = cur_headers; - so_local.types = cur_types; - so_local.cells.reserve((size_t)cur_rows * cur_cols_n); - for (int i = 0; i < cur_rows * cur_cols_n; ++i) - so_local.cells.push_back(cur_cells[i]); - so_ptr = &so_local; - } - int close_idx = -1; - for (int i = 0; i < (int)st.extra_panels.size(); ++i) { - if (draw_extra_panel(st, st.extra_panels[i], i, *so_ptr)) close_idx = i; - } - if (close_idx >= 0) st.extra_panels.erase(st.extra_panels.begin() + close_idx); - } - } - - // ---------- Modales (comunes a ambos paths) ---------- - if (U.cf_open) ImGui::OpenPopup("Custom column"); - if (ImGui::BeginPopupModal("Custom column", &U.cf_open, - ImGuiWindowFlags_AlwaysAutoResize)) - { - ImGui::Text("Nombre:"); - char name_buf[128] = {0}; - std::snprintf(name_buf, sizeof(name_buf), "%s", U.cf_name.c_str()); - ImGui::SetNextItemWidth(520); - if (ImGui::InputText("##cfname", name_buf, sizeof(name_buf))) U.cf_name = name_buf; - - ImGui::Spacing(); - ImGui::Text("Formula (Lua). Acceso celdas via row. o row[idx]."); - ImGui::TextDisabled("Ejemplo: return row.size_kb * 1024"); - - static char formula_buf[4096] = {0}; - if (U.cf_force_cursor || std::strcmp(formula_buf, U.cf_formula.c_str()) != 0) { - std::snprintf(formula_buf, sizeof(formula_buf), "%s", U.cf_formula.c_str()); - } - ImGuiInputTextFlags flags = - ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways; - if (ImGui::InputTextMultiline("##cfformula", formula_buf, sizeof(formula_buf), - ImVec2(520, 200), flags, autocomplete_cb, &U)) { - U.cf_formula = formula_buf; - } - if (U.cf_ac_open) { - ImVec2 box_min = ImGui::GetItemRectMin(); - ImVec2 box_max = ImGui::GetItemRectMax(); - ImGui::SetNextWindowPos(ImVec2(box_min.x + 20, box_max.y + 4)); - ImGui::SetNextWindowSize(ImVec2(280, 0)); - ImGuiWindowFlags wf = - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | - ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_AlwaysAutoResize; - if (ImGui::Begin("##colpicker", nullptr, wf)) { - ImGui::TextDisabled("Pick column:"); - ImGui::Separator(); - auto ci_contains = [](const std::string& hay, const std::string& nd) { - if (nd.empty()) return true; - std::string a = hay, b = nd; - for (char& c : a) if (c >= 'A' && c <= 'Z') c += 32; - for (char& c : b) if (c >= 'A' && c <= 'Z') c += 32; - return a.find(b) != std::string::npos; - }; - int shown = 0; - for (int c = 0; c < eff_cols && shown < 12; ++c) { - std::string nm = eff_headers[c]; - if (!ci_contains(nm, U.cf_ac_filter)) continue; - char lbl[200]; - std::snprintf(lbl, sizeof(lbl), "%s %s", - column_type_icon(eff_types[c]), nm.c_str()); - if (ImGui::Selectable(lbl)) { - int new_cursor = 0; - std::string updated = insert_column_ref( - U.cf_formula, U.cf_ac_start, U.cf_ac_cursor, nm, new_cursor); - U.cf_formula = updated; - U.cf_target_cursor= new_cursor; - U.cf_force_cursor = true; - U.cf_ac_open = false; - } - ++shown; - } - if (shown == 0) ImGui::TextDisabled("(sin matches)"); - } - ImGui::End(); - } - - if (!U.cf_error.empty()) { - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(230, 100, 100, 255)); - ImGui::TextWrapped("Error: %s", U.cf_error.c_str()); - ImGui::PopStyleColor(); - } - - if (ImGui::Button("Compile & save")) { - std::string err; - int lid = lua_engine::compile(lua_engine::get(), U.cf_formula, &err); - if (lid < 0) { - U.cf_error = err; - } else { - // Build sample context segun cf_target_stage. - // target == 0: usa orig cells + stage 0 derived. - // target > 0: recomputa chain hasta el target (excluyendo - // derived del target) y sample sobre ese output. - int ts = U.cf_target_stage; - if (ts < 0 || ts >= (int)st.stages.size()) ts = 0; - int sample = 0; - std::vector samples_str; - - if (ts == 0) { - sample = std::min(64, row_count); - 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 = &name_to_col; - ctx.types_orig = eff_types.data(); - ctx.n_types_orig = orig_cols; - ctx.derived = &stage0.derived; - ctx.derived_name_to_idx = &derived_n2i; - std::string e; - samples_str.emplace_back( - lua_engine::eval(lua_engine::get(), lid, ctx, &e)); - } - } else { - // Recompute chain hasta stage ts output (sin aplicar derived - // del propio ts). - State st_sample = st; - // Limpia derived del target stage para que el sample no - // se referencie a si mismo. - if (ts < (int)st_sample.stages.size()) - st_sample.stages[ts].derived.clear(); - // Reusa la logica de materializacion: simple recompute manual. - // Aplica stage 0 (orig + derived) materializado. - State stmp = st; - Stage& s0 = stmp.stages[0]; - for (auto& f : s0.filters) { - if (f.col >= 0 && f.col < eff_cols) f.col = src_for_eff[f.col]; - } - s0.sorts.clear(); - auto v0 = compute_visible_rows(cells, row_count, orig_cols, stmp); - - std::vector mb; - std::vector mc; - mb.reserve((size_t)v0.size() * eff_cols); - mc.reserve((size_t)v0.size() * eff_cols); - for (int r : v0) { - for (int c = 0; c < eff_cols; ++c) { - if (c < orig_cols) { - const char* p = cells[r * orig_cols + c]; - mb.emplace_back(p ? p : ""); - } else { - const DerivedColumn& d = stage0.derived[c - orig_cols]; - if (!d.formula.empty() && d.lua_id >= 0) { - lua_engine::RowCtx ctx; - ctx.cells = cells; ctx.orig_cols = orig_cols; - ctx.row = r; - ctx.header_names = &hn_storage; - ctx.name_to_col = &name_to_col; - ctx.types_orig = eff_types.data(); - ctx.n_types_orig = orig_cols; - ctx.derived = &stage0.derived; - ctx.derived_name_to_idx = &derived_n2i; - std::string e; - mb.emplace_back(lua_engine::eval(lua_engine::get(), d.lua_id, ctx, &e)); - } else if (d.source_col >= 0) { - const char* p = cells[r * orig_cols + d.source_col]; - mb.emplace_back(p ? p : ""); - } else mb.emplace_back(""); - } - } - } - for (auto& s : mb) mc.push_back(s.c_str()); - - std::vector ch(eff_cols); - std::vector ct(eff_cols); - for (int c = 0; c < eff_cols; ++c) { ch[c] = eff_headers[c]; ct[c] = eff_types[c]; } - - const char* const* cc = mc.data(); - int cr = (int)v0.size(); - int cn = eff_cols; - std::vector tmps; - for (int si = 1; si <= ts; ++si) { - Stage stage_sn = st.stages[si]; - // En el target stage NO apliques sus propias derived. - if (si == ts) stage_sn.derived.clear(); - tmps.push_back(compute_stage(cc, cr, cn, ch, ct, stage_sn)); - const StageOutput& l = tmps.back(); - cc = l.cells.data(); cr = l.rows; cn = l.cols; - ch = l.headers; ct = l.types; - } - // Build name_to_col map for the target stage output. - std::vector hn_t = ch; - std::unordered_map n2c_t; - for (size_t i = 0; i < hn_t.size(); ++i) n2c_t[hn_t[i]] = (int)i; - sample = std::min(64, cr); - for (int r = 0; r < sample; ++r) { - lua_engine::RowCtx ctx; - ctx.cells = cc; - ctx.orig_cols = cn; - ctx.row = r; - ctx.header_names = &hn_t; - ctx.name_to_col = &n2c_t; - ctx.types_orig = ct.data(); - ctx.n_types_orig = cn; - std::string e; - samples_str.emplace_back( - lua_engine::eval(lua_engine::get(), lid, ctx, &e)); - } - } - - std::vector samples_ptr; - samples_ptr.reserve(samples_str.size()); - for (auto& s : samples_str) samples_ptr.push_back(s.c_str()); - ColumnType auto_t = auto_detect_type(samples_ptr.data(), - (int)samples_ptr.size(), 1, 0); - - // Save to target stage. - if (ts < 0 || ts >= (int)st.stages.size()) ts = 0; - auto& target_derived = st.stages[ts].derived; - if (U.cf_editing && U.cf_edit_idx >= 0 && - U.cf_edit_idx < (int)target_derived.size()) - { - auto& d = target_derived[U.cf_edit_idx]; - if (d.lua_id >= 0) lua_engine::release(lua_engine::get(), d.lua_id); - d.formula = U.cf_formula; - d.name = U.cf_name.empty() ? "custom" : U.cf_name; - d.type = auto_t; - d.lua_id = lid; - d.compile_error.clear(); - } else { - DerivedColumn d; - d.source_col = -1; - d.type = auto_t; - d.name = U.cf_name.empty() ? "custom" : U.cf_name; - d.formula = U.cf_formula; - d.lua_id = lid; - target_derived.push_back(d); - } - U.cf_open = false; - U.cf_error.clear(); - } - } - ImGui::SameLine(); - if (ImGui::Button("Cancel")) { - U.cf_open = false; - U.cf_error.clear(); - } - ImGui::EndPopup(); - } - - if (U.tql_show_open) ImGui::OpenPopup("Show TQL"); - if (ImGui::BeginPopupModal("Show TQL", &U.tql_show_open, - ImGuiWindowFlags_AlwaysAutoResize)) - { - ImGui::Text("TQL serializado del estado actual (read-only):"); - ImGui::InputTextMultiline("##tqlshow", U.tql_show_text.data(), - U.tql_show_text.size() + 1, - ImVec2(560, 280), - ImGuiInputTextFlags_ReadOnly); - if (ImGui::Button("Copy to clipboard")) { - ImGui::SetClipboardText(U.tql_show_text.c_str()); - } - ImGui::SameLine(); - if (ImGui::Button("Close")) U.tql_show_open = false; - ImGui::EndPopup(); - } - - if (U.tql_apply_open) ImGui::OpenPopup("Apply TQL"); - if (ImGui::BeginPopupModal("Apply TQL", &U.tql_apply_open, - ImGuiWindowFlags_AlwaysAutoResize)) - { - ImGui::Text("Pega un chunk TQL (Lua). Ver docs/TQL.md para sintaxis."); - static char tql_buf[8192] = {0}; - if (std::strcmp(tql_buf, U.tql_apply_text.c_str()) != 0) { - std::snprintf(tql_buf, sizeof(tql_buf), "%s", U.tql_apply_text.c_str()); - } - if (ImGui::InputTextMultiline("##tqlapply", tql_buf, sizeof(tql_buf), - ImVec2(560, 280))) { - U.tql_apply_text = tql_buf; - } - if (!U.tql_apply_error.empty()) { - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(230, 100, 100, 255)); - ImGui::TextWrapped("Error: %s", U.tql_apply_error.c_str()); - ImGui::PopStyleColor(); - } - if (ImGui::Button("Apply")) { - std::vector orig_headers(orig_cols); - std::vector orig_types(orig_cols); - for (int c = 0; c < orig_cols; ++c) { - orig_headers[c] = headers[c]; - orig_types[c] = eff_types[c]; - } - std::string err; - bool ok = tql::apply(U.tql_apply_text, st, orig_headers, orig_types, - cells, row_count, orig_cols, &err); - if (ok) { - U.tql_apply_open = false; - U.tql_apply_error.clear(); - } else { - U.tql_apply_error = err; - } - } - ImGui::SameLine(); - if (ImGui::Button("Cancel")) { - U.tql_apply_open = false; - U.tql_apply_error.clear(); - } - ImGui::EndPopup(); - } - - // Ask AI modal (fase 11 — issue 0080). - if (U.ask_open) ImGui::OpenPopup("Ask AI"); - ImGui::SetNextWindowSize(ImVec2(820, 560), ImGuiCond_Appearing); - if (ImGui::BeginPopupModal("Ask AI", &U.ask_open, - ImGuiWindowFlags_NoSavedSettings)) { - ImGui::TextDisabled("Ask en lenguaje natural. Default TQL. SQL solo si DuckDB linkado."); - const char* modes[] = {"TQL", "SQL (DuckDB)"}; -#ifndef FN_TQL_DUCKDB - // SQL mode disabled visually pero el toggle existe (informativo) - if (U.ask_mode == 1) U.ask_mode = 0; -#endif - ImGui::Combo("Output##askmode", &U.ask_mode, modes, IM_ARRAYSIZE(modes)); -#ifndef FN_TQL_DUCKDB - if (U.ask_mode == 1) { - ImGui::TextColored(ImVec4(1, 0.5f, 0.3f, 1), - "SQL mode requires FN_TQL_DUCKDB=1 build flag."); - } -#endif - ImGui::InputTextMultiline("##ask_q", U.ask_question, sizeof(U.ask_question), - ImVec2(-1, 80)); - ImGui::BeginDisabled(U.ask_busy); - if (ImGui::Button("Send")) { - U.ask_busy = true; - U.ask_status = "Sending..."; - U.ask_error.clear(); - U.ask_response_code.clear(); - U.ask_response_raw.clear(); - - // Build AskInput desde el state actual. - llm_anthropic::AskInput in; - in.question = U.ask_question; - in.tql_current = U.ask_current_tql; - in.col_names = U.active_headers; - in.col_types = U.active_types; - in.mode = (U.ask_mode == 1) - ? llm_anthropic::OutputMode::SQL - : llm_anthropic::OutputMode::TQL; - - // Llamada blocking (UI freeze breve durante red). - auto r = llm_anthropic::ask(in); - U.ask_busy = false; - if (!r.error.empty()) { - U.ask_error = r.error; - U.ask_status = "Error"; - } else { - U.ask_response_raw = r.raw; - U.ask_response_code = r.code; - U.ask_status = "Got response."; - // Llenar edit buffer - std::snprintf(U.ask_edit_buf, sizeof(U.ask_edit_buf), - "%s", r.code.c_str()); - } - } - ImGui::EndDisabled(); - ImGui::SameLine(); - if (!U.ask_status.empty()) { - ImGui::TextDisabled("%s", U.ask_status.c_str()); - } - if (!U.ask_error.empty()) { - ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", U.ask_error.c_str()); - } - ImGui::Separator(); - ImGui::Columns(2, "ask_cols", true); - ImGui::TextUnformatted("Current"); - ImGui::InputTextMultiline("##ask_cur", - const_cast(U.ask_current_tql.c_str()), - U.ask_current_tql.size() + 1, - ImVec2(-1, 240), - ImGuiInputTextFlags_ReadOnly); - ImGui::NextColumn(); - ImGui::TextUnformatted("Proposed (editable before apply)"); - ImGui::InputTextMultiline("##ask_new", U.ask_edit_buf, sizeof(U.ask_edit_buf), - ImVec2(-1, 240)); - ImGui::Columns(1); - - bool can_apply = !U.ask_busy && U.ask_edit_buf[0] != '\0'; - ImGui::BeginDisabled(!can_apply); - if (ImGui::Button("Apply")) { - std::string err; - if (U.ask_mode == 0) { - // TQL apply - bool ok = tql::apply(U.ask_edit_buf, st, - U.active_headers, - U.active_types, - nullptr, 0, - (int)U.active_headers.size(), - &err); - if (ok) { - U.ask_status = "Applied OK."; - U.ask_open = false; - } else { - U.ask_error = "tql::apply error: " + err; - U.ask_status = "Apply failed."; - } - } else { -#ifdef FN_TQL_DUCKDB - // SQL apply: ejecutar via tql_duckdb sobre TableInputs activas. - // Para tablas en memoria construimos un TableInput basico desde - // active_headers/types. v1 no recupera cells originales aqui; - // reportamos solo error si fallo. Caller real deberia pasar - // tables() del render scope. Sin esto, marcamos status info. - U.ask_status = "SQL execute disponible (FN_TQL_DUCKDB ON). " - "Integracion full pendiente: usar tql_duckdb::execute desde caller."; -#else - U.ask_status = "SQL execute requires FN_TQL_DUCKDB build flag."; -#endif - } - } - ImGui::EndDisabled(); - ImGui::SameLine(); - if (ImGui::Button("Reject")) { - U.ask_response_code.clear(); - U.ask_edit_buf[0] = '\0'; - } - ImGui::SameLine(); - if (ImGui::Button("Close")) { - U.ask_open = false; - } - ImGui::EndPopup(); - } - - if (U.open_cell_popup) { ImGui::OpenPopup("##cell_op"); U.open_cell_popup = false; } - if (ImGui::BeginPopup("##cell_op")) { - ColumnType t = (U.pending_col >= 0 && U.pending_col < eff_cols) - ? eff_types[U.pending_col] : ColumnType::String; - const char* hdr = (U.pending_col >= 0 && U.pending_col < eff_cols) - ? eff_headers[U.pending_col] : "?"; - ImGui::TextDisabled("%s %s ?? \"%s\"", - column_type_icon(t), hdr, U.pending_value.c_str()); - ImGui::Separator(); - auto ops = ops_for_type(t); - for (Op o : ops) { - if (ImGui::MenuItem(op_label(o))) { - st.stages[0].filters.push_back({U.pending_col, o, U.pending_value}); - ImGui::CloseCurrentPopup(); - } - } - ImGui::EndPopup(); - } -} - -} // namespace data_table diff --git a/cpp/apps/primitives_gallery/playground/tables/data_table.h b/cpp/apps/primitives_gallery/playground/tables/data_table.h deleted file mode 100644 index f553b1c8..00000000 --- a/cpp/apps/primitives_gallery/playground/tables/data_table.h +++ /dev/null @@ -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& tables, - State& st, - bool show_chrome = true); - -} // namespace data_table diff --git a/cpp/apps/primitives_gallery/playground/tables/data_table_logic.cpp b/cpp/apps/primitives_gallery/playground/tables/data_table_logic.cpp deleted file mode 100644 index f81e09e6..00000000 --- a/cpp/apps/primitives_gallery/playground/tables/data_table_logic.cpp +++ /dev/null @@ -1,1495 +0,0 @@ -#include "data_table_logic.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace data_table { - -const char* op_label(Op o) { - switch (o) { - 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 "contains"; - case Op::NotContains: return "!contains"; - case Op::StartsWith: return "starts"; - case Op::EndsWith: return "ends"; - } - return "?"; -} - -bool op_is_string_only(Op o) { - return o == Op::Contains || o == Op::NotContains || - o == Op::StartsWith || o == Op::EndsWith; -} - -const char* column_type_name(ColumnType t) { - switch (t) { - case ColumnType::Auto: return "auto"; - 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"; - } - return "?"; -} - -// Icons Tabler (UTF-8). Mantenidos como strings para no forzar include de icons_tabler.h aqui. -const char* column_type_icon(ColumnType t) { - switch (t) { - case ColumnType::Auto: return "\xef\xa4\x9d"; // TI_HELP_CIRCLE - case ColumnType::String: return "\xef\x95\xa7"; // TI_ABC - case ColumnType::Int: return "\xef\x95\x94"; // TI_123 - case ColumnType::Float: return "\xef\xa8\xa6"; // TI_DECIMAL - case ColumnType::Bool: return "\xee\xae\xa6"; // TI_CHECKBOX - case ColumnType::Date: return "\xee\xa9\x93"; // TI_CALENDAR - case ColumnType::Json: return "\xee\xaf\x8c"; // TI_BRACES - } - return "?"; -} - -std::vector ops_for_type(ColumnType t) { - switch (t) { - case ColumnType::Int: - case ColumnType::Float: - case ColumnType::Date: - return {Op::Eq, Op::Neq, Op::Gt, Op::Gte, Op::Lt, Op::Lte}; - case ColumnType::Bool: - return {Op::Eq, Op::Neq}; - case ColumnType::Json: - return {Op::Eq, Op::Neq, Op::Contains, Op::NotContains}; - case ColumnType::String: - return {Op::Eq, Op::Neq, Op::Contains, Op::NotContains, Op::StartsWith, Op::EndsWith}; - case ColumnType::Auto: - default: - return {Op::Eq, Op::Neq, Op::Contains, Op::NotContains}; - } -} - -namespace { - -bool is_bool_text(const char* s) { - return std::strcmp(s, "true") == 0 || std::strcmp(s, "false") == 0; -} - -bool is_date_iso(const char* s) { - // YYYY-MM-DD minimo - if (std::strlen(s) < 10) return false; - auto d = [](char c){ return c >= '0' && c <= '9'; }; - return d(s[0]) && d(s[1]) && d(s[2]) && d(s[3]) && s[4] == '-' && - d(s[5]) && d(s[6]) && s[7] == '-' && d(s[8]) && d(s[9]); -} - -bool is_json_text(const char* s) { - while (*s == ' ' || *s == '\t') ++s; - return *s == '{' || *s == '['; -} - -bool is_integer_text(const char* s) { - if (!*s) return false; - if (*s == '-' || *s == '+') ++s; - if (!*s) return false; - for (; *s; ++s) if (*s < '0' || *s > '9') return false; - return true; -} - -} // anon - -ColumnType auto_detect_type(const char* const* cells, int rows, int cols, - int col, int sample_n) -{ - if (col < 0 || col >= cols) return ColumnType::String; - int n_total = 0, n_int = 0, n_float = 0, n_bool = 0, n_date = 0, n_json = 0; - for (int r = 0; r < rows && n_total < sample_n; ++r) { - const char* c = cells[r * cols + col]; - if (!c || !*c) continue; - n_total++; - if (is_bool_text(c)) { n_bool++; continue; } - if (is_date_iso(c)) { n_date++; continue; } - if (is_json_text(c)) { n_json++; continue; } - double v; - if (parse_number(c, v)) { - if (is_integer_text(c)) n_int++; - else n_float++; - continue; - } - // string: no se cuenta a ningun tipo -> garantiza fallthrough a String - } - if (n_total == 0) return ColumnType::String; - if (n_bool == n_total) return ColumnType::Bool; - if (n_date == n_total) return ColumnType::Date; - if (n_json == n_total) return ColumnType::Json; - if (n_int + n_float == n_total) return (n_float > 0) ? ColumnType::Float : ColumnType::Int; - return ColumnType::String; -} - -ColumnType effective_type(ColumnType declared, const char* const* cells, - int rows, int cols, int col) -{ - if (declared != ColumnType::Auto) return declared; - return auto_detect_type(cells, rows, cols, col); -} - -bool parse_number(const char* s, double& out) { - if (!s || !*s) return false; - char* end = nullptr; - double v = std::strtod(s, &end); - if (end == s) return false; - while (*end == ' ' || *end == '\t') end++; - if (*end != '\0') return false; - out = v; - return true; -} - -bool compare(const char* a, const char* b, Op op) { - if (!a) a = ""; - if (!b) b = ""; - // Ops solo de string (siempre lexical, no intentan numeric). - switch (op) { - case Op::Contains: return std::strstr(a, b) != nullptr; - case Op::NotContains: return std::strstr(a, b) == nullptr; - case Op::StartsWith: { - size_t lb = std::strlen(b); - return std::strncmp(a, b, lb) == 0; - } - case Op::EndsWith: { - size_t la = std::strlen(a), lb = std::strlen(b); - return lb <= la && std::strcmp(a + la - lb, b) == 0; - } - default: break; - } - double na, nb; - bool numeric = parse_number(a, na) && parse_number(b, nb); - if (numeric) { - switch (op) { - case Op::Eq: return na == nb; - case Op::Neq: return na != nb; - case Op::Gt: return na > nb; - case Op::Gte: return na >= nb; - case Op::Lt: return na < nb; - case Op::Lte: return na <= nb; - default: break; - } - } - int c = std::strcmp(a, b); - switch (op) { - case Op::Eq: return c == 0; - case Op::Neq: return c != 0; - case Op::Gt: return c > 0; - case Op::Gte: return c >= 0; - case Op::Lt: return c < 0; - case Op::Lte: return c <= 0; - default: break; - } - return false; -} - -// Helpers de State para acceso a stages. -void State::ensure_stage0() { - if (stages.empty()) stages.push_back(Stage{}); - if (active_stage < 0) active_stage = 0; - if (active_stage >= (int)stages.size()) active_stage = (int)stages.size() - 1; -} -Stage& State::raw() { ensure_stage0(); return stages[0]; } -const Stage& State::raw() const { - static thread_local Stage empty; - if (stages.empty()) return empty; - return stages[0]; -} -Stage& State::active() { - ensure_stage0(); - return stages[active_stage]; -} -const Stage& State::active_const() const { - static thread_local Stage empty; - if (stages.empty()) return empty; - int a = active_stage; - if (a < 0 || a >= (int)stages.size()) a = 0; - return stages[a]; -} - -// Compatibilidad: aplica filters + primer sort del stage 0 (Raw). Si el state -// no tiene stages, devuelve todas las filas sin filtrar. Util para tests y -// para el render path actual (que solo opera sobre Raw cuando no hay grouping). -std::vector compute_visible_rows(const char* const* cells, - int rows, int cols, - const State& st) -{ - std::vector out; - out.reserve(rows); - const Stage& s = st.raw(); - for (int r = 0; r < rows; ++r) { - bool keep = true; - for (const auto& f : s.filters) { - if (f.col < 0 || f.col >= cols) continue; - const char* cell = cells[r * cols + f.col]; - if (!compare(cell, f.value.c_str(), f.op)) { keep = false; break; } - } - if (keep) out.push_back(r); - } - if (!s.sorts.empty()) { - // El stage 0 stores sorts as {col_name, desc}. Para compat, si el - // nombre es vacio o "@idx", interpretamos como indice numerico. - const SortClause& sc0 = s.sorts.front(); - int sc = -1; - // Permitir nombre numerico estilo "@idx" o lookup posicional via - // primer caracter '@'. Sino, busqueda por header no posible aqui - // (no tenemos headers) — devuelve sin sort. Para compat de tests - // usamos nombre "@N" donde N es indice 0-based. - if (!sc0.col.empty() && sc0.col[0] == '@') { - sc = std::atoi(sc0.col.c_str() + 1); - } - bool desc = sc0.desc; - if (sc >= 0 && sc < cols) { - std::sort(out.begin(), out.end(), [&](int a, int b) { - const char* ca = cells[a * cols + sc]; - const char* cb = cells[b * cols + sc]; - if (!ca) ca = ""; - if (!cb) cb = ""; - double na, nb; - bool num = parse_number(ca, na) && parse_number(cb, nb); - int cmp; - if (num) cmp = (na < nb) ? -1 : (na > nb ? 1 : 0); - else cmp = std::strcmp(ca, cb); - return desc ? (cmp > 0) : (cmp < 0); - }); - } - } - return out; -} - -ColStats compute_column_stats(const char* const* cells, int rows, int cols, - int col, int unique_cap, - const int* indices, int n_indices) -{ - ColStats s; - if (col < 0 || col >= cols) return s; - bool use_idx = (indices != nullptr && n_indices > 0); - int n = use_idx ? n_indices : rows; - s.total = n; - std::unordered_map counts; - if (unique_cap > 0) counts.reserve(std::min(unique_cap, n)); - bool all_numeric = true; - std::vector nums; - nums.reserve(n); - for (int i = 0; i < n; ++i) { - int r = use_idx ? indices[i] : i; - if (r < 0 || r >= rows) continue; - const char* c = cells[r * cols + col]; - if (!c || !*c) { s.empty_count++; continue; } - double v; - if (parse_number(c, v)) { - if (s.numeric_count == 0) { s.min = v; s.max = v; } - else { - if (v < s.min) s.min = v; - if (v > s.max) s.max = v; - } - s.sum += v; - s.numeric_count++; - nums.push_back(v); - } else { - all_numeric = false; - } - if (unique_cap == 0 || (int)counts.size() < unique_cap) { - counts[c]++; - } else { - auto it = counts.find(c); - if (it != counts.end()) it->second++; - else s.unique_capped = true; - } - } - s.unique_count = (int)counts.size(); - s.numeric = all_numeric && s.numeric_count > 0; - if (s.numeric_count > 0) s.mean = s.sum / s.numeric_count; - // Top 8 categorias por count desc. - if (!counts.empty()) { - std::vector> v(counts.begin(), counts.end()); - int topN = std::min(8, (int)v.size()); - std::partial_sort(v.begin(), v.begin() + topN, v.end(), - [](const auto& a, const auto& b){ return a.second > b.second; }); - v.resize(topN); - s.top_categories = std::move(v); - } - if (s.numeric && !nums.empty()) { - std::sort(nums.begin(), nums.end()); - auto pct = [&](double p) { - double idx = p * (nums.size() - 1); - size_t lo = (size_t)idx; - size_t hi = std::min(lo + 1, nums.size() - 1); - double t = idx - lo; - return nums[lo] * (1.0 - t) + nums[hi] * t; - }; - s.p25 = pct(0.25); - s.p50 = pct(0.50); - s.p75 = pct(0.75); - - s.hist.assign(HIST_BINS, 0.0f); - double range = s.max - s.min; - if (range <= 0) { - s.hist[HIST_BINS / 2] = (float)nums.size(); - } else { - for (double v : nums) { - int b = (int)((v - s.min) / range * HIST_BINS); - if (b < 0) b = 0; - if (b >= HIST_BINS) b = HIST_BINS - 1; - s.hist[b] += 1.0f; - } - } - } - return s; -} - -void reorder_column(State& st, int src, int dst) { - if (src == dst) return; - auto it_s = std::find(st.col_order.begin(), st.col_order.end(), src); - auto it_d = std::find(st.col_order.begin(), st.col_order.end(), dst); - if (it_s == st.col_order.end() || it_d == st.col_order.end()) return; - int si = (int)(it_s - st.col_order.begin()); - int di = (int)(it_d - st.col_order.begin()); - int v = st.col_order[si]; - st.col_order.erase(st.col_order.begin() + si); - // Insertar en `di`: cubre ambos sentidos. Para si insert(di) lo - // coloca al final de la posicion logica original de dst. Para si>di - // (drag izquierda) dst sigue en di y src queda antes. - if (di > (int)st.col_order.size()) di = (int)st.col_order.size(); - st.col_order.insert(st.col_order.begin() + di, v); -} - -std::string csv_escape(const char* s) { - if (!s) return ""; - bool needs = false; - for (const char* p = s; *p; ++p) { - if (*p == ',' || *p == '"' || *p == '\n' || *p == '\r') { needs = true; break; } - } - if (!needs) return std::string(s); - std::string out; out.reserve(std::strlen(s) + 4); - out += '"'; - for (const char* p = s; *p; ++p) { - if (*p == '"') out += '"'; - out += *p; - } - out += '"'; - return out; -} - -namespace { -std::string tsv_sanitize(const char* s) { - std::string out; - if (!s) return out; - out.reserve(std::strlen(s)); - for (const char* p = s; *p; ++p) { - char ch = *p; - if (ch == '\t' || ch == '\n' || ch == '\r') ch = ' '; - out += ch; - } - return out; -} -} // anon - -std::string build_tsv(const char* const* cells, int rows, int cols, - const char* const* headers, - const std::vector& col_order, - const std::vector& col_visible, - const std::vector& visible_rows, - int view_row_lo, int view_row_hi, - int view_col_lo, int view_col_hi) -{ - if (col_order.empty() || visible_rows.empty()) return ""; - int rmin = std::min(view_row_lo, view_row_hi); - int rmax = std::max(view_row_lo, view_row_hi); - int cmin = std::min(view_col_lo, view_col_hi); - int cmax = std::max(view_col_lo, view_col_hi); - rmin = std::max(0, rmin); - rmax = std::min((int)visible_rows.size() - 1, rmax); - cmin = std::max(0, cmin); - cmax = std::min((int)col_order.size() - 1, cmax); - - std::string out; - bool first = true; - for (int oc = cmin; oc <= cmax; ++oc) { - int c = col_order[oc]; - if (c < 0 || c >= cols) continue; - if (c < (int)col_visible.size() && !col_visible[c]) continue; - if (!first) out += '\t'; - out += tsv_sanitize(headers[c]); - first = false; - } - out += '\n'; - for (int ri = rmin; ri <= rmax; ++ri) { - int r = visible_rows[ri]; - first = true; - for (int oc = cmin; oc <= cmax; ++oc) { - int c = col_order[oc]; - if (c < 0 || c >= cols) continue; - if (c < (int)col_visible.size() && !col_visible[c]) continue; - if (!first) out += '\t'; - out += tsv_sanitize(cells[r * cols + c]); - first = false; - } - out += '\n'; - } - return out; -} - -std::string build_csv(const char* const* cells, int rows, int cols, - const char* const* headers, - const std::vector& col_order, - const std::vector& col_visible, - const std::vector& visible_rows) -{ - if (col_order.empty()) return ""; - std::string out; - bool first = true; - for (int oc = 0; oc < (int)col_order.size(); ++oc) { - int c = col_order[oc]; - if (c < 0 || c >= cols) continue; - if (c < (int)col_visible.size() && !col_visible[c]) continue; - if (!first) out += ','; - out += csv_escape(headers[c]); - first = false; - } - out += '\n'; - for (int r : visible_rows) { - first = true; - for (int oc = 0; oc < (int)col_order.size(); ++oc) { - int c = col_order[oc]; - if (c < 0 || c >= cols) continue; - if (c < (int)col_visible.size() && !col_visible[c]) continue; - if (!first) out += ','; - out += csv_escape(cells[r * cols + c]); - first = false; - } - out += '\n'; - } - return out; -} - -int find_open_bracket(const char* buf, int len, int cursor, std::string& filter_text) { - filter_text.clear(); - if (!buf || cursor <= 0 || cursor > len) return -1; - for (int i = cursor - 1; i >= 0; --i) { - char c = buf[i]; - if (c == ']' || c == '\n') return -1; // already closed or new line - if (c == '[') { - filter_text.assign(buf + i + 1, cursor - i - 1); - return i; - } - } - return -1; -} - -std::string insert_column_ref(const std::string& src, int start, int cursor, - const std::string& name, int& new_cursor) -{ - if (start < 0 || start > (int)src.size() || cursor < start || cursor > (int)src.size()) { - new_cursor = cursor; - return src; - } - std::string replacement = "[" + name + "]"; - std::string out; - out.reserve(src.size() - (cursor - start) + replacement.size()); - out.append(src, 0, start); - out += replacement; - out.append(src, cursor, std::string::npos); - new_cursor = start + (int)replacement.size(); - return out; -} - -// ---------------------------------------------------------------------------- -// TQL stage compute -// ---------------------------------------------------------------------------- - -const char* agg_fn_name(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 "?"; -} - -std::string aggregation_alias(const Aggregation& a) { - if (!a.alias.empty()) return a.alias; - if (a.fn == AggFn::Count) return "count"; - if (a.fn == AggFn::Percentile) { - int pct = (int)(a.arg * 100.0 + 0.5); - char buf[128]; - std::snprintf(buf, sizeof(buf), "p%d_%s", pct, a.col.c_str()); - return buf; - } - std::string out = agg_fn_name(a.fn); - out += '_'; - out += a.col; - return out; -} - -ColumnType aggregation_type(const Aggregation& a, - const std::vector& in_headers, - const std::vector& in_types) -{ - if (a.fn == AggFn::Count || a.fn == AggFn::Distinct) return ColumnType::Int; - if (a.fn == AggFn::Min || a.fn == AggFn::Max) { - for (size_t i = 0; i < in_headers.size(); ++i) { - if (in_headers[i] == a.col && i < in_types.size()) return in_types[i]; - } - return ColumnType::String; - } - return ColumnType::Float; -} - -Filter make_drill_filter(int col_idx, const std::string& value) { - Filter f; - f.col = col_idx; - f.op = Op::Eq; - f.value = value; - return f; -} - -bool apply_drill_step(State& st, const DrillStep& step) { - if (step.target_stage < 0 || step.target_stage >= (int)st.stages.size()) return false; - Stage& s = st.stages[step.target_stage]; - int pos = step.filter_pos; - if (pos < 0 || pos > (int)s.filters.size()) return false; - s.filters.insert(s.filters.begin() + pos, step.added); - st.active_stage = step.target_stage; - return true; -} - -bool drill_up(State& st) { - if (st.stages.empty()) return false; - if (st.active_stage <= 0) return false; - st.active_stage -= 1; - return true; -} - -std::string row_to_tsv(const char* const* cells, int rows, int cols, - int row_idx, const std::vector& headers) { - if (row_idx < 0 || row_idx >= rows || cols <= 0) return ""; - std::string out; - for (int c = 0; c < cols; ++c) { - if (c > 0) out += '\t'; - if (c < (int)headers.size()) out += headers[c]; - } - out += "\r\n"; - for (int c = 0; c < cols; ++c) { - if (c > 0) out += '\t'; - const char* v = cells[row_idx * cols + c]; - if (v) out += v; - } - out += "\r\n"; - return out; -} - -std::vector build_filters_from_row(const char* const* cells, int rows, - int cols, int row_idx) { - std::vector out; - if (row_idx < 0 || row_idx >= rows || cols <= 0) return out; - for (int c = 0; c < cols; ++c) { - const char* v = cells[row_idx * cols + c]; - if (!v || !*v) continue; - Filter f; - f.col = c; - f.op = Op::Eq; - f.value = v; - out.push_back(f); - } - return out; -} - -bool undo_drill_step(State& st, const DrillStep& step) { - if (step.target_stage < 0 || step.target_stage >= (int)st.stages.size()) return false; - Stage& s = st.stages[step.target_stage]; - int pos = step.filter_pos; - if (pos < 0 || pos >= (int)s.filters.size()) return false; - s.filters.erase(s.filters.begin() + pos); - if (step.prev_active_stage >= 0 && step.prev_active_stage < (int)st.stages.size()) { - st.active_stage = step.prev_active_stage; - } - return true; -} - -std::vector apply_filters(const char* const* cells, int rows, int cols, - const std::vector& filters) -{ - std::vector out; - out.reserve(rows); - for (int r = 0; r < rows; ++r) { - bool keep = true; - for (const auto& f : filters) { - if (f.col < 0 || f.col >= cols) continue; - const char* cell = cells[r * cols + f.col]; - if (!compare(cell, f.value.c_str(), f.op)) { keep = false; break; } - } - if (keep) out.push_back(r); - } - return out; -} - -namespace { - -int find_col(const std::vector& headers, const std::string& name) { - for (size_t i = 0; i < headers.size(); ++i) if (headers[i] == name) return (int)i; - return -1; -} - -// Compara dos cells para sort: numerico si ambos parseables, sino lexical. -int cmp_cells(const char* a, const char* b) { - if (!a) a = ""; if (!b) b = ""; - double na, nb; - bool num = parse_number(a, na) && parse_number(b, nb); - if (num) return (na < nb) ? -1 : (na > nb ? 1 : 0); - return std::strcmp(a, b); -} - -void apply_sorts(std::vector& row_idx, - const char* const* cells, int cols, - const std::vector& headers, - const std::vector& sorts) -{ - if (sorts.empty()) return; - std::vector sort_cols(sorts.size()); - for (size_t i = 0; i < sorts.size(); ++i) sort_cols[i] = find_col(headers, sorts[i].col); - std::sort(row_idx.begin(), row_idx.end(), [&](int a, int b){ - for (size_t i = 0; i < sorts.size(); ++i) { - int sc = sort_cols[i]; - if (sc < 0) continue; - int c = cmp_cells(cells[a * cols + sc], cells[b * cols + sc]); - if (c != 0) return sorts[i].desc ? (c > 0) : (c < 0); - } - return false; - }); -} - -double percentile_value(std::vector& v, double p) { - if (v.empty()) return 0.0; - std::sort(v.begin(), v.end()); - double idx = p * (v.size() - 1); - size_t lo = (size_t)idx; - size_t hi = std::min(lo + 1, v.size() - 1); - double t = idx - lo; - return v[lo] * (1.0 - t) + v[hi] * t; -} - -double compute_agg_numeric(AggFn fn, std::vector& vals, double arg) { - if (vals.empty()) return 0.0; - switch (fn) { - case AggFn::Sum: { - double s = 0; for (double v : vals) s += v; return s; - } - case AggFn::Avg: { - double s = 0; for (double v : vals) s += v; return s / vals.size(); - } - case AggFn::Min: { - double m = vals[0]; for (double v : vals) if (v < m) m = v; return m; - } - case AggFn::Max: { - double m = vals[0]; for (double v : vals) if (v > m) m = v; return m; - } - case AggFn::Stddev: { - double s = 0; for (double v : vals) s += v; - double mean = s / vals.size(); - double var = 0; for (double v : vals) { double d = v - mean; var += d * d; } - return std::sqrt(var / vals.size()); - } - case AggFn::Median: return percentile_value(vals, 0.50); - case AggFn::P25: return percentile_value(vals, 0.25); - case AggFn::P75: return percentile_value(vals, 0.75); - case AggFn::P90: return percentile_value(vals, 0.90); - case AggFn::P99: return percentile_value(vals, 0.99); - case AggFn::Percentile: return percentile_value(vals, arg); - default: return 0.0; - } -} - -std::string format_double(double v) { - char buf[64]; - long long iv = (long long)v; - if ((double)iv == v) std::snprintf(buf, sizeof(buf), "%lld", iv); - else std::snprintf(buf, sizeof(buf), "%.4g", v); - return buf; -} - -} // anon - -StageOutput compute_stage(const char* const* in_cells, int in_rows, int in_cols, - const std::vector& in_headers, - const std::vector& in_types, - const Stage& stage) -{ - StageOutput out; - auto visible = apply_filters(in_cells, in_rows, in_cols, stage.filters); - - bool grouped = !stage.breakouts.empty() || !stage.aggregations.empty(); - - if (!grouped) { - // Passthrough: misma forma, filtrado + ordenado. - out.cols = in_cols; - out.headers = in_headers; - out.types = in_types; - // Sort sobre visible. - apply_sorts(visible, in_cells, in_cols, in_headers, stage.sorts); - out.rows = (int)visible.size(); - out.cells.reserve((size_t)out.rows * in_cols); - for (int r : visible) { - for (int c = 0; c < in_cols; ++c) out.cells.push_back(in_cells[r * in_cols + c]); - } - return out; - } - - // Grouped: agrupa visible por valores de breakout, calcula aggregations. - // Breakouts pueden llevar sufijo `:granularity` para cols Date (fase 10). - int nbreaks = (int)stage.breakouts.size(); - std::vector break_cols(nbreaks); - std::vector break_grans(nbreaks); - bool any_trunc = false; - for (int i = 0; i < nbreaks; ++i) { - std::string col_name; - break_grans[i] = parse_breakout_granularity(stage.breakouts[i], col_name); - if (break_grans[i] != DateGranularity::None) any_trunc = true; - break_cols[i] = find_col(in_headers, col_name); - } - - // Pre-truncate solo cuando hay granularity activa. Strings persistidos en - // out.cell_backing para que los punteros sobrevivan al return de la funcion. - // Reservamos upfront para que push_back no invalide punteros anteriores. - // Tamaño = trunc cells + aggregation cells (peor caso n_groups <= in_rows). - out.cell_backing.reserve( - (size_t)in_rows * (size_t)nbreaks + - (size_t)in_rows * stage.aggregations.size() + 16); - - std::vector trunc_ptrs; - if (any_trunc) { - trunc_ptrs.assign((size_t)in_rows * (size_t)nbreaks, nullptr); - for (int r = 0; r < in_rows; ++r) { - for (int i = 0; i < nbreaks; ++i) { - if (break_grans[i] == DateGranularity::None) continue; - int bc = break_cols[i]; - if (bc < 0) continue; - const char* v = in_cells[r * in_cols + bc]; - out.cell_backing.emplace_back( - truncate_date(v ? v : "", break_grans[i])); - trunc_ptrs[(size_t)r * nbreaks + i] = out.cell_backing.back().c_str(); - } - } - } - - auto cell_for = [&](int r, int i) -> const char* { - int bc = break_cols[i]; - if (bc < 0) return ""; - if (break_grans[i] != DateGranularity::None) { - return trunc_ptrs[(size_t)r * nbreaks + i]; - } - const char* v = in_cells[r * in_cols + bc]; - return v ? v : ""; - }; - - auto make_key = [&](int r) -> std::string { - std::string k; - for (int i = 0; i < nbreaks; ++i) { - if (i > 0) k += '\x1f'; // separador unit-separator (no aparece en datos) - k += cell_for(r, i); - } - return k; - }; - - // Mantenemos orden de aparicion para estabilidad pre-sort. - std::unordered_map key_to_group; - std::vector group_keys; // canonical, no usado salvo debug - std::vector> group_rows; // indices en in_cells por grupo - std::vector> group_breakvals; // valores break por grupo - for (int r : visible) { - std::string k = make_key(r); - auto it = key_to_group.find(k); - int gi; - if (it == key_to_group.end()) { - gi = (int)group_rows.size(); - key_to_group.emplace(k, gi); - group_keys.push_back(k); - group_rows.emplace_back(); - std::vector bv((size_t)nbreaks, ""); - for (int i = 0; i < nbreaks; ++i) { - bv[i] = cell_for(r, i); - } - group_breakvals.push_back(std::move(bv)); - } else gi = it->second; - group_rows[gi].push_back(r); - } - - // Headers + types del output: breakouts + aggregation aliases. - int out_cols = (int)stage.breakouts.size() + (int)stage.aggregations.size(); - out.cols = out_cols; - out.headers.reserve(out_cols); - out.types.reserve(out_cols); - for (int i = 0; i < nbreaks; ++i) { - out.headers.push_back(stage.breakouts[i]); - int bc = break_cols[i]; - // Si hay granularity activa, el output es String (formato ymd o similar), - // no la fecha original. - ColumnType ot = ColumnType::String; - if (break_grans[i] == DateGranularity::None - && bc >= 0 && bc < (int)in_types.size()) { - ot = in_types[bc]; - } - out.types.push_back(ot); - } - for (const auto& a : stage.aggregations) { - out.headers.push_back(aggregation_alias(a)); - out.types.push_back(aggregation_type(a, in_headers, in_types)); - } - - // Compute aggregation values por grupo. Reservamos backing con tamaño exacto - // para que los punteros .c_str() no se invaliden. - int n_groups = (int)group_rows.size(); - out.cell_backing.reserve((size_t)n_groups * stage.aggregations.size() + 16); - - auto store_backing = [&](const std::string& s) -> const char* { - out.cell_backing.push_back(s); - return out.cell_backing.back().c_str(); - }; - - // Construimos cells por grupo (filas no ordenadas todavia). - std::vector flat; - flat.reserve((size_t)n_groups * out_cols); - for (int gi = 0; gi < n_groups; ++gi) { - // breakout values: punteros directos a in_cells (estables). - for (size_t i = 0; i < stage.breakouts.size(); ++i) { - flat.push_back(group_breakvals[gi][i]); - } - // aggregations - for (const auto& a : stage.aggregations) { - if (a.fn == AggFn::Count) { - flat.push_back(store_backing(format_double((double)group_rows[gi].size()))); - continue; - } - if (a.fn == AggFn::Distinct) { - int ac = find_col(in_headers, a.col); - if (ac < 0) { flat.push_back(store_backing("0")); continue; } - std::unordered_set uniq; - for (int r : group_rows[gi]) { - const char* v = in_cells[r * in_cols + ac]; - if (v && *v) uniq.insert(v); - } - flat.push_back(store_backing(format_double((double)uniq.size()))); - continue; - } - int ac = find_col(in_headers, a.col); - if (ac < 0) { flat.push_back(store_backing("")); continue; } - // min/max sobre strings preserva tipo - if ((a.fn == AggFn::Min || a.fn == AggFn::Max) && - ac < (int)in_types.size() && - (in_types[ac] == ColumnType::String || in_types[ac] == ColumnType::Date)) - { - const char* best = nullptr; - for (int r : group_rows[gi]) { - const char* v = in_cells[r * in_cols + ac]; - if (!v || !*v) continue; - if (!best) { best = v; continue; } - int c = std::strcmp(v, best); - if ((a.fn == AggFn::Min && c < 0) || (a.fn == AggFn::Max && c > 0)) best = v; - } - flat.push_back(best ? best : store_backing("")); - continue; - } - std::vector vals; - vals.reserve(group_rows[gi].size()); - for (int r : group_rows[gi]) { - const char* v = in_cells[r * in_cols + ac]; - if (!v || !*v) continue; - double d; - if (parse_number(v, d)) vals.push_back(d); - } - double agg_val = compute_agg_numeric(a.fn, vals, a.arg); - flat.push_back(store_backing(format_double(agg_val))); - } - } - - // Sort sobre los n_groups segun stage.sorts (col-name lookup en out.headers). - std::vector grp_idx(n_groups); - for (int i = 0; i < n_groups; ++i) grp_idx[i] = i; - apply_sorts(grp_idx, flat.data(), out_cols, out.headers, stage.sorts); - - out.rows = n_groups; - out.cells.reserve((size_t)n_groups * out_cols); - for (int gi : grp_idx) { - for (int c = 0; c < out_cols; ++c) { - out.cells.push_back(flat[gi * out_cols + c]); - } - } - return out; -} - -// ---------------------------------------------------------------------------- -// ViewMode helpers -// ---------------------------------------------------------------------------- -struct ViewModeInfo { - ViewMode m; - const char* token; - const char* label; - int min_cols; - bool needs_num; - bool needs_cat; - bool needs_agg; -}; - -static const ViewModeInfo kViewModes[] = { - { ViewMode::Table, "table", "Table", 1, false, false, false }, - { ViewMode::Bar, "bar", "Bar (horizontal)", 2, true, true, true }, - { ViewMode::Column, "column", "Column (vertical)", 2, true, true, true }, - { ViewMode::GroupedBar, "grouped_bar", "Grouped bar", 2, true, true, true }, - { ViewMode::StackedBar, "stacked_bar", "Stacked bar", 2, true, true, true }, - { ViewMode::Line, "line", "Line", 1, true, false, false }, - { ViewMode::Area, "area", "Area", 1, true, false, false }, - { ViewMode::Stairs, "stairs", "Stairs", 1, true, false, false }, - { ViewMode::Scatter, "scatter", "Scatter", 2, true, false, false }, - { ViewMode::Bubble, "bubble", "Bubble", 3, true, false, false }, - { ViewMode::Histogram, "histogram", "Histogram", 1, true, false, false }, - { ViewMode::Histogram2D, "hist2d", "Histogram 2D", 2, true, false, false }, - { ViewMode::Heatmap, "heatmap", "Heatmap", 1, true, false, false }, - { ViewMode::BoxPlot, "boxplot", "Box plot", 2, true, true, false }, - { ViewMode::Stem, "stem", "Stem", 1, true, false, false }, - { ViewMode::ErrorBars, "errorbars", "Error bars", 2, true, false, false }, - { ViewMode::Pie, "pie", "Pie", 2, true, true, true }, - { ViewMode::Donut, "donut", "Donut", 2, true, true, true }, - { ViewMode::Funnel, "funnel", "Funnel", 2, true, true, true }, - { ViewMode::Waterfall, "waterfall", "Waterfall", 1, true, false, true }, - { ViewMode::KPI, "kpi", "KPI (single)", 1, true, false, true }, - { ViewMode::KPIGrid, "kpi_grid", "KPI grid", 1, true, false, true }, - { ViewMode::Candlestick, "candlestick", "Candlestick (OHLC)", 4, true, false, false }, - { ViewMode::Radar, "radar", "Radar", 2, true, true, false }, -}; -static const int kViewModesN = (int)(sizeof(kViewModes) / sizeof(kViewModes[0])); - -const char* view_mode_token(ViewMode m) { - for (int i = 0; i < kViewModesN; ++i) if (kViewModes[i].m == m) return kViewModes[i].token; - return "table"; -} - -const char* view_mode_label(ViewMode m) { - for (int i = 0; i < kViewModesN; ++i) if (kViewModes[i].m == m) return kViewModes[i].label; - return "Table"; -} - -ViewMode view_mode_from_token(const char* s) { - if (!s) return ViewMode::Table; - for (int i = 0; i < kViewModesN; ++i) { - if (std::strcmp(kViewModes[i].token, s) == 0) return kViewModes[i].m; - } - return ViewMode::Table; -} - -int view_mode_min_cols(ViewMode m) { - for (int i = 0; i < kViewModesN; ++i) if (kViewModes[i].m == m) return kViewModes[i].min_cols; - return 1; -} - -bool view_mode_needs_numeric(ViewMode m) { - for (int i = 0; i < kViewModesN; ++i) if (kViewModes[i].m == m) return kViewModes[i].needs_num; - return false; -} - -bool view_mode_needs_category(ViewMode m) { - for (int i = 0; i < kViewModesN; ++i) if (kViewModes[i].m == m) return kViewModes[i].needs_cat; - return false; -} - -bool view_mode_needs_aggregation(ViewMode m) { - for (int i = 0; i < kViewModesN; ++i) if (kViewModes[i].m == m) return kViewModes[i].needs_agg; - return false; -} - -const ViewMode* all_view_modes(int* n_out) { - static ViewMode arr[64]; - static bool init = false; - if (!init) { - for (int i = 0; i < kViewModesN; ++i) arr[i] = kViewModes[i].m; - init = true; - } - if (n_out) *n_out = kViewModesN; - return arr; -} - -// ---------------------------------------------------------------------------- -// Joins -// ---------------------------------------------------------------------------- -int resolve_main_idx(const std::vector& tables, const std::string& main_source) { - if (tables.empty()) return -1; - if (main_source.empty()) return 0; - for (size_t i = 0; i < tables.size(); ++i) { - if (tables[i].name == main_source) return (int)i; - } - return 0; -} - -const char* join_strategy_token(JoinStrategy s) { - switch (s) { - case JoinStrategy::Left: return "left"; - case JoinStrategy::Inner: return "inner"; - case JoinStrategy::Right: return "right"; - case JoinStrategy::Full: return "full"; - } - return "left"; -} - -JoinStrategy join_strategy_from_token(const char* s) { - if (!s) return JoinStrategy::Left; - if (std::strcmp(s, "inner") == 0) return JoinStrategy::Inner; - if (std::strcmp(s, "right") == 0) return JoinStrategy::Right; - if (std::strcmp(s, "full") == 0) return JoinStrategy::Full; - return JoinStrategy::Left; -} - -const char* join_strategy_label(JoinStrategy s) { - switch (s) { - case JoinStrategy::Left: return "left-join"; - case JoinStrategy::Inner: return "inner-join"; - case JoinStrategy::Right: return "right-join"; - case JoinStrategy::Full: return "full-join"; - } - return "left-join"; -} - -namespace { - -int find_col_idx(const std::vector& hdrs, const std::string& name) { - for (size_t i = 0; i < hdrs.size(); ++i) if (hdrs[i] == name) return (int)i; - return -1; -} - -std::string make_key(const char* const* cells, int row, int cols, - const std::vector& key_cols) { - std::string k; - for (int c : key_cols) { - if (c < 0 || c >= cols) { k += "\x1f|"; continue; } - const char* s = cells[row * cols + c]; - k += (s ? s : ""); - k += "\x1f"; // separator - } - return k; -} - -} // anon - -StageOutput join_tables(const char* const* left_cells, int left_rows, int left_cols, - const std::vector& left_headers, - const std::vector& left_types, - const TableInput& right, - const Join& jn) -{ - StageOutput out; - - // Resolver indices de keys en left y right. - std::vector lk_idx, rk_idx; - for (const auto& p : jn.on) { - lk_idx.push_back(find_col_idx(left_headers, p.first)); - rk_idx.push_back(find_col_idx(right.headers, p.second)); - } - - // Resolver fields del derecho a incluir. - std::vector right_fields; - if (jn.fields.empty()) { - for (int i = 0; i < right.cols; ++i) right_fields.push_back(i); - } else { - for (const auto& f : jn.fields) { - int i = find_col_idx(right.headers, f); - if (i >= 0) right_fields.push_back(i); - } - } - - // Build output headers + types: left + alias.right_field. - out.cols = left_cols + (int)right_fields.size(); - out.headers.reserve(out.cols); - out.types.reserve(out.cols); - for (int c = 0; c < left_cols; ++c) { - out.headers.push_back(c < (int)left_headers.size() ? left_headers[c] : ""); - out.types.push_back(c < (int)left_types.size() ? left_types[c] : ColumnType::Auto); - } - for (int rc : right_fields) { - std::string prefixed = jn.alias.empty() ? right.headers[rc] : (jn.alias + "." + right.headers[rc]); - out.headers.push_back(std::move(prefixed)); - out.types.push_back(rc < (int)right.types.size() ? right.types[rc] : ColumnType::Auto); - } - - // Hash right rows por key. - std::unordered_map> right_idx; - right_idx.reserve(right.rows); - for (int r = 0; r < right.rows; ++r) { - right_idx[make_key(right.cells, r, right.cols, rk_idx)].push_back(r); - } - - // Marca cuales right rows fueron usados (para right/full). - std::vector right_matched(right.rows, false); - - // Backing strings para celdas. - out.cell_backing.reserve((size_t)(left_rows + right.rows) * out.cols); - - auto append_left_row = [&](int lr) { - for (int c = 0; c < left_cols; ++c) { - const char* s = left_cells[lr * left_cols + c]; - out.cell_backing.emplace_back(s ? s : ""); - } - }; - auto append_left_empty = [&]() { - for (int c = 0; c < left_cols; ++c) out.cell_backing.emplace_back(""); - }; - auto append_right_row = [&](int rr) { - for (int rc : right_fields) { - const char* s = right.cells[rr * right.cols + rc]; - out.cell_backing.emplace_back(s ? s : ""); - } - }; - auto append_right_empty = [&]() { - for (int rc : right_fields) { (void)rc; out.cell_backing.emplace_back(""); } - }; - - bool include_left = (jn.strategy == JoinStrategy::Left || jn.strategy == JoinStrategy::Inner || - jn.strategy == JoinStrategy::Full); - bool keep_unmatched_left = (jn.strategy == JoinStrategy::Left || jn.strategy == JoinStrategy::Full); - bool keep_unmatched_right = (jn.strategy == JoinStrategy::Right || jn.strategy == JoinStrategy::Full); - - int row_count = 0; - - if (include_left || jn.strategy == JoinStrategy::Right) { - for (int lr = 0; lr < left_rows; ++lr) { - std::string k = make_key(left_cells, lr, left_cols, lk_idx); - auto it = right_idx.find(k); - if (it == right_idx.end() || it->second.empty()) { - if (keep_unmatched_left) { - append_left_row(lr); - append_right_empty(); - ++row_count; - } - continue; - } - for (int rr : it->second) { - append_left_row(lr); - append_right_row(rr); - right_matched[rr] = true; - ++row_count; - } - } - } - - if (keep_unmatched_right) { - for (int rr = 0; rr < right.rows; ++rr) { - if (right_matched[rr]) continue; - append_left_empty(); - append_right_row(rr); - ++row_count; - } - } - - out.rows = row_count; - - // Punteros tras llenar backing. - out.cells.reserve(out.cell_backing.size()); - for (auto& s : out.cell_backing) out.cells.push_back(s.c_str()); - return out; -} - -// ---------------------------------------------------------------------------- -// Fase 10: drill extendido — granularity + presets. -// ---------------------------------------------------------------------------- - -const char* date_granularity_token(DateGranularity g) { - switch (g) { - case DateGranularity::Year: return "year"; - case DateGranularity::Month: return "month"; - case DateGranularity::Week: return "week"; - case DateGranularity::Day: return "day"; - case DateGranularity::Hour: return "hour"; - default: return ""; - } -} - -DateGranularity date_granularity_from_token(const char* s) { - if (!s) return DateGranularity::None; - std::string t(s); - if (t == "year") return DateGranularity::Year; - if (t == "month") return DateGranularity::Month; - if (t == "week") return DateGranularity::Week; - if (t == "day") return DateGranularity::Day; - if (t == "hour") return DateGranularity::Hour; - return DateGranularity::None; -} - -DateGranularity parse_breakout_granularity(const std::string& breakout, - std::string& col_out) { - auto pos = breakout.rfind(':'); - if (pos == std::string::npos) { - col_out = breakout; - return DateGranularity::None; - } - std::string suffix = breakout.substr(pos + 1); - DateGranularity g = date_granularity_from_token(suffix.c_str()); - if (g == DateGranularity::None) { - col_out = breakout; - return DateGranularity::None; - } - col_out = breakout.substr(0, pos); - return g; -} - -std::string compose_breakout(const std::string& col, DateGranularity g) { - if (g == DateGranularity::None) return col; - return col + ":" + date_granularity_token(g); -} - -int nearest_index_1d(double target, const double* xs, int n) { - if (n <= 0 || !xs) return -1; - int best = -1; - double best_d = 0.0; - for (int i = 0; i < n; ++i) { - double v = xs[i]; - if (std::isnan(v)) continue; - double d = std::fabs(v - target); - if (best < 0 || d < best_d) { best = i; best_d = d; } - } - return best; -} - -int nearest_index_2d(double tx, double ty, - const double* xs, const double* ys, int n) { - if (n <= 0 || !xs || !ys) return -1; - int best = -1; - double best_d = 0.0; - for (int i = 0; i < n; ++i) { - double x = xs[i], y = ys[i]; - if (std::isnan(x) || std::isnan(y)) continue; - double dx = x - tx, dy = y - ty; - double d = dx*dx + dy*dy; - if (best < 0 || d < best_d) { best = i; best_d = d; } - } - return best; -} - -double pie_angle(double cx, double cy, double mx, double my) { - // ImPlot pie: 0 = top, sentido horario. atan2 estandar: 0 = +X (right), CCW. - // Conversion: ImPlot angle = atan2(dx, -dy) y normalizar a [0, 2*PI). - double dx = mx - cx; - double dy = my - cy; - double a = std::atan2(dx, -dy); // 0 cuando (dx=0, dy<0) = top - const double two_pi = 6.283185307179586; - if (a < 0) a += two_pi; - return a; -} - -int pie_slice_at_angle(double angle, const double* sums, int n) { - if (n <= 0 || !sums) return -1; - double total = 0.0; - for (int i = 0; i < n; ++i) { - if (sums[i] < 0) return -1; - total += sums[i]; - } - if (total <= 0.0) return -1; - const double two_pi = 6.283185307179586; - if (angle < 0 || angle >= two_pi) return -1; - double cum = 0.0; - for (int i = 0; i < n; ++i) { - cum += (sums[i] / total) * two_pi; - if (angle < cum) return i; - } - return n - 1; // edge case rounding -} - -void heatmap_cell_at(double px, double py, int rows, int cols, - int& row_out, int& col_out) { - row_out = -1; - col_out = -1; - if (rows <= 0 || cols <= 0) return; - if (px < 0.0 || px >= (double)cols) return; - if (py < 0.0 || py >= (double)rows) return; - col_out = (int)px; - // ImPlot heatmap pinta row 0 arriba; plot Y suele invertirse. Caller - // normaliza si necesita. Aqui devolvemos row = floor(py) en coord plot. - row_out = (int)py; -} - -void column_min_max(const char* const* cells, int rows, int cols, int col_idx, - std::string& min_out, std::string& max_out) { - min_out.clear(); - max_out.clear(); - if (col_idx < 0 || col_idx >= cols) return; - bool first = true; - for (int r = 0; r < rows; ++r) { - const char* v = cells[r * cols + col_idx]; - if (!v || !*v) continue; - std::string s(v); - if (first) { - min_out = s; - max_out = s; - first = false; - } else { - if (s < min_out) min_out = s; - if (s > max_out) max_out = s; - } - } -} - -namespace { - -// Parse ISO "YYYY-MM-DD..." -> (y, m, d). True si los 3 primeros campos OK. -bool parse_ymd(const std::string& s, int& y, int& m, int& d) { - if (s.size() < 10) return false; - for (int i : {0,1,2,3,5,6,8,9}) { - if (s[(size_t)i] < '0' || s[(size_t)i] > '9') return false; - } - if (s[4] != '-' || s[7] != '-') return false; - y = (s[0]-'0')*1000 + (s[1]-'0')*100 + (s[2]-'0')*10 + (s[3]-'0'); - m = (s[5]-'0')*10 + (s[6]-'0'); - d = (s[8]-'0')*10 + (s[9]-'0'); - if (m < 1 || m > 12 || d < 1 || d > 31) return false; - return true; -} - -// Dias desde 0001-01-01 (proleptic Gregorian). -long ymd_to_days(int y, int m, int d) { - if (m <= 2) { y -= 1; m += 12; } - long era = (y >= 0 ? y : y - 399) / 400; - unsigned yoe = (unsigned)(y - era * 400); - unsigned doy = (unsigned)((153 * (m - 3) + 2) / 5 + d - 1); - unsigned doe = yoe * 365 + yoe/4 - yoe/100 + doy; - return era * 146097 + (long)doe; -} - -void days_to_ymd(long days, int& y, int& m, int& d) { - long era = (days >= 0 ? days : days - 146096) / 146097; - unsigned doe = (unsigned)(days - era * 146097); - unsigned yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365; - int yr = (int)yoe + (int)era * 400; - unsigned doy = doe - (365*yoe + yoe/4 - yoe/100); - unsigned mp = (5*doy + 2)/153; - unsigned day = doy - (153*mp + 2)/5 + 1; - unsigned mon = mp < 10 ? mp + 3 : mp - 9; - if (mon <= 2) yr += 1; - y = yr; m = (int)mon; d = (int)day; -} - -} // anon - -std::string truncate_date(const std::string& date, DateGranularity g) { - if (g == DateGranularity::None) return date; - int y, m, d; - if (!parse_ymd(date, y, m, d)) return date; - char buf[32]; - switch (g) { - case DateGranularity::Year: - std::snprintf(buf, sizeof(buf), "%04d", y); - return buf; - case DateGranularity::Month: - std::snprintf(buf, sizeof(buf), "%04d-%02d", y, m); - return buf; - case DateGranularity::Day: - std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d", y, m, d); - return buf; - case DateGranularity::Hour: { - int hh = 0; - if (date.size() >= 13 && date[10] == 'T' - && date[11] >= '0' && date[11] <= '9' - && date[12] >= '0' && date[12] <= '9') { - hh = (date[11]-'0')*10 + (date[12]-'0'); - if (hh < 0 || hh > 23) hh = 0; - } - std::snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d", y, m, d, hh); - return buf; - } - case DateGranularity::Week: { - // Hinnant ymd_to_days: day 0 == 0000-03-01 (Wednesday). - // days%7: 0=Wed, 1=Thu, 2=Fri, 3=Sat, 4=Sun, 5=Mon, 6=Tue. - // Monday offset: (mod - 5 + 7) % 7. - long days = ymd_to_days(y, m, d); - int mod = (int)(((days % 7) + 7) % 7); - int rem = ((mod - 5) % 7 + 7) % 7; - long monday = days - rem; - int yy, mm, dd; - days_to_ymd(monday, yy, mm, dd); - std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d", yy, mm, dd); - return buf; - } - default: return date; - } -} - -DateGranularity auto_date_granularity(const std::string& min_ymd, - const std::string& max_ymd) { - int y1,m1,d1, y2,m2,d2; - if (!parse_ymd(min_ymd, y1,m1,d1)) return DateGranularity::Day; - if (!parse_ymd(max_ymd, y2,m2,d2)) return DateGranularity::Day; - long span = ymd_to_days(y2,m2,d2) - ymd_to_days(y1,m1,d1); - if (span < 0) span = -span; - if (span > 730) return DateGranularity::Year; // >2 anios - if (span > 60) return DateGranularity::Month; - if (span > 14) return DateGranularity::Week; - return DateGranularity::Day; -} - -const char* filter_preset_label(FilterPreset p) { - switch (p) { - case FilterPreset::Last7d: return "Last 7 days"; - case FilterPreset::Last30d: return "Last 30 days"; - case FilterPreset::Last90d: return "Last 90 days"; - case FilterPreset::ExcludeNulls: return "Exclude nulls"; - case FilterPreset::NonZero: return "Non-zero only"; - } - return "?"; -} - -std::vector build_preset_filters(FilterPreset preset, int col, - const std::string& today_ymd) { - std::vector out; - auto last_n = [&](int n) { - int y, m, d; - if (!parse_ymd(today_ymd, y, m, d)) return; - long days = ymd_to_days(y, m, d) - n; - int yy, mm, dd; - days_to_ymd(days, yy, mm, dd); - char buf[16]; - std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d", yy, mm, dd); - Filter f; - f.col = col; - f.op = Op::Gte; - f.value = buf; - out.push_back(f); - }; - switch (preset) { - case FilterPreset::Last7d: last_n(7); break; - case FilterPreset::Last30d: last_n(30); break; - case FilterPreset::Last90d: last_n(90); break; - case FilterPreset::ExcludeNulls: { - Filter f; f.col = col; f.op = Op::Neq; f.value = ""; - out.push_back(f); - break; - } - case FilterPreset::NonZero: { - Filter f1; f1.col = col; f1.op = Op::Neq; f1.value = ""; - Filter f2; f2.col = col; f2.op = Op::Neq; f2.value = "0"; - out.push_back(f1); - out.push_back(f2); - break; - } - } - return out; -} - -} // namespace data_table diff --git a/cpp/apps/primitives_gallery/playground/tables/data_table_logic.h b/cpp/apps/primitives_gallery/playground/tables/data_table_logic.h deleted file mode 100644 index 8ab4b06c..00000000 --- a/cpp/apps/primitives_gallery/playground/tables/data_table_logic.h +++ /dev/null @@ -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 -#include -#include - -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 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_" -// percentile p -> "p_" (ej. p95_size_kb) -// resto -> "_" (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& in_headers, - const std::vector& 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& in_headers, - const std::vector& in_types, - const Stage& stage); - -// Pure: aplica filtros usando headers para resolver f.col. -std::vector apply_filters(const char* const* cells, int rows, int cols, - const std::vector& 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& 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& left_headers, - const std::vector& 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& headers); - -// Pure (fase 10): construye filters Op::Eq desde una fila. -std::vector 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 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 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& col_order, - const std::vector& col_visible, - const std::vector& 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& col_order, - const std::vector& col_visible, - const std::vector& 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 hist; - std::vector> 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 diff --git a/cpp/apps/primitives_gallery/playground/tables/e2e_run.sh b/cpp/apps/primitives_gallery/playground/tables/e2e_run.sh deleted file mode 100755 index 3e264b54..00000000 --- a/cpp/apps/primitives_gallery/playground/tables/e2e_run.sh +++ /dev/null @@ -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" diff --git a/cpp/apps/primitives_gallery/playground/tables/llm_anthropic.h b/cpp/apps/primitives_gallery/playground/tables/llm_anthropic.h deleted file mode 100644 index bbc35c03..00000000 --- a/cpp/apps/primitives_gallery/playground/tables/llm_anthropic.h +++ /dev/null @@ -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 -#include - -namespace llm_anthropic { - -enum class OutputMode { TQL, SQL }; - -struct AskInput { - std::string question; // pregunta NL - std::string tql_current; // TQL actual (emitido) - std::vector col_names; // schema input - std::vector col_types; - std::vector 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 ```\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 diff --git a/cpp/apps/primitives_gallery/playground/tables/lua_engine.h b/cpp/apps/primitives_gallery/playground/tables/lua_engine.h deleted file mode 100644 index daf4e375..00000000 --- a/cpp/apps/primitives_gallery/playground/tables/lua_engine.h +++ /dev/null @@ -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 -#include -#include - -// 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* header_names = nullptr; - const std::unordered_map* 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* derived = nullptr; - const std::unordered_map* 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 diff --git a/cpp/apps/primitives_gallery/playground/tables/main.cpp b/cpp/apps/primitives_gallery/playground/tables/main.cpp deleted file mode 100644 index 7bbf9e2b..00000000 --- a/cpp/apps/primitives_gallery/playground/tables/main.cpp +++ /dev/null @@ -1,191 +0,0 @@ -#include "app_base.h" -#include "imgui.h" -#include "core/logger.h" -#include "data_table.h" - -#include -#include -#include -#include -#include -#include - -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 backing; // dynamic strings (name, version, deps, size, cov, date) - std::vector cells; // row-major pointers -}; - -const char* const* dataset_cells(const Dataset& d) { return d.cells.data(); } - -std::shared_ptr build_dataset(int rows) { - auto d = std::make_shared(); - 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& current_dataset() { - static std::shared_ptr 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(types, types + 10); - main_t.cells = dataset_cells(d); - main_t.rows = d.rows; - main_t.cols = d.cols; - - std::vector 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(fn_log::Level::Info)} - }, render); -} -#endif diff --git a/cpp/apps/primitives_gallery/playground/tables/self_test.cpp b/cpp/apps/primitives_gallery/playground/tables/self_test.cpp deleted file mode 100644 index ed1ed217..00000000 --- a/cpp/apps/primitives_gallery/playground/tables/self_test.cpp +++ /dev/null @@ -1,2921 +0,0 @@ -// E2E self-test del playground tables. Ejercita la logica pura -// (data_table_logic) sin ImGui. Build target separado: -// -// tables_playground_self_test -> linux -// tables_playground_self_test.exe -> windows -// -// Exit 0 = todos los checks pasan, 1 = falla. - -#include "data_table_logic.h" -#include "llm_anthropic.h" -#include "lua_engine.h" -#include "tql.h" -#include "tql_to_sql.h" -#ifdef FN_TQL_DUCKDB -# include "tql_duckdb.h" -#endif - -#include -#include -#include -#include -#include -#include -#include - -namespace { - -int failed = 0; -int passed = 0; - -void check(bool cond, const char* name) { - if (cond) { passed++; std::printf("PASS %s\n", name); } - else { failed++; std::printf("FAIL %s\n", name); } -} - -} // namespace - -using namespace data_table; - -// Test helpers: imitan la API antigua sort_col/sort_desc sobre el nuevo -// modelo de SortClause-by-name. Usan la convencion "@" para indices -// posicionales (compatible con compute_visible_rows). -namespace { -void set_sort_idx(State& st, int idx, bool desc) { - st.ensure_stage0(); - st.stages[0].sorts.clear(); - if (idx < 0) return; - char buf[16]; std::snprintf(buf, sizeof(buf), "@%d", idx); - st.stages[0].sorts.push_back({buf, desc}); -} -void set_sort_desc(State& st, bool desc) { - st.ensure_stage0(); - if (st.stages[0].sorts.empty()) return; - st.stages[0].sorts.front().desc = desc; -} -int sort_col_idx(const State& st) { - const Stage& s = st.raw(); - if (s.sorts.empty()) return -1; - const std::string& c = s.sorts.front().col; - if (c.size() < 2 || c[0] != '@') return -1; - return std::atoi(c.c_str() + 1); -} -bool sort_col_desc(const State& st) { - const Stage& s = st.raw(); - if (s.sorts.empty()) return false; - return s.sorts.front().desc; -} -} // namespace - -int main() { - // --- parse_number --- - double v = 0; - check(parse_number("1.23", v) && v == 1.23, "parse_number 1.23"); - check(parse_number("42", v) && v == 42.0, "parse_number 42"); - check(parse_number("-7.5", v) && v == -7.5, "parse_number -7.5"); - check(!parse_number("abc", v), "parse_number abc rejected"); - check(!parse_number("12x", v), "parse_number 12x rejected"); - check(!parse_number("", v), "parse_number empty rejected"); - check(!parse_number(nullptr, v), "parse_number null rejected"); - - // --- compare numerico --- - check( compare("10", "2", Op::Gt), "10 > 2 numerico"); - check(!compare("10", "2", Op::Lt), "10 < 2 numerico false"); - check( compare("2", "10", Op::Lt), "2 < 10 numerico"); - check( compare("5", "5", Op::Eq), "5 == 5 numerico"); - check( compare("5", "5", Op::Gte), "5 >= 5 numerico"); - check( compare("5", "5", Op::Lte), "5 <= 5 numerico"); - check( compare("5", "5", Op::Neq) == false, "5 != 5 numerico false"); - - // --- compare lexical (cuando no son numeros) --- - check( compare("go", "go", Op::Eq), "lexical eq"); - check( compare("go", "py", Op::Neq), "lexical neq"); - check( compare("py", "go", Op::Gt), "lexical gt"); - check( compare("ab", "ac", Op::Lt), "lexical lt"); - - // --- compute_visible_rows: filter --- - const char* cells[] = { - "a","1", - "b","2", - "c","3", - "a","4", - }; - State st; - st.raw().filters.push_back({0, Op::Eq, "a"}); - auto rows = compute_visible_rows(cells, 4, 2, st); - check(rows.size() == 2 && rows[0] == 0 && rows[1] == 3, "filter col0 = a"); - - // --- filter numerico --- - st.raw().filters.clear(); - st.raw().filters.push_back({1, Op::Gt, "2"}); - rows = compute_visible_rows(cells, 4, 2, st); - check(rows.size() == 2 && rows[0] == 2 && rows[1] == 3, "filter col1 > 2"); - - // --- combinacion: > 1 AND col0 != b --- - st.raw().filters.clear(); - st.raw().filters.push_back({1, Op::Gt, "1"}); - st.raw().filters.push_back({0, Op::Neq, "b"}); - rows = compute_visible_rows(cells, 4, 2, st); - check(rows.size() == 2 && rows[0] == 2 && rows[1] == 3, "filter combinado AND"); - - // --- sort ascendente numerico --- - st.raw().filters.clear(); - set_sort_idx(st, 1, false); - set_sort_desc(st, false); - rows = compute_visible_rows(cells, 4, 2, st); - check(rows.size() == 4 && rows[0] == 0 && rows[3] == 3, "sort asc numerico"); - - // --- sort descendente numerico --- - set_sort_desc(st, true); - rows = compute_visible_rows(cells, 4, 2, st); - check(rows.size() == 4 && rows[0] == 3 && rows[3] == 0, "sort desc numerico"); - - // --- sort lexical --- - set_sort_idx(st, 0, false); - set_sort_desc(st, false); - rows = compute_visible_rows(cells, 4, 2, st); - check(rows.size() == 4 && std::strcmp(cells[rows[0]*2], "a") == 0 - && std::strcmp(cells[rows[3]*2], "c") == 0, "sort asc lexical"); - - // --- filter + sort combinado --- - set_sort_idx(st, 1, false); - set_sort_desc(st, true); - st.raw().filters.push_back({0, Op::Eq, "a"}); - rows = compute_visible_rows(cells, 4, 2, st); - check(rows.size() == 2 && rows[0] == 3 && rows[1] == 0, "filter+sort combinado"); - - // --- filter sobre columna inexistente: se ignora --- - st.raw().filters.clear(); - st.raw().filters.push_back({99, Op::Eq, "x"}); - set_sort_idx(st, -1, false); - rows = compute_visible_rows(cells, 4, 2, st); - check(rows.size() == 4, "filter col fuera de rango ignorado"); - - // --- col_order default identidad tras init --- - State st2; - st2.col_order = {0, 1, 2, 3}; - check(st2.col_order.size() == 4 && st2.col_order[0] == 0 && st2.col_order[3] == 3, - "col_order identidad"); - - // --- col_order no afecta compute_visible_rows (sort/filter trabajan sobre col dataset) --- - st2.col_order = {3, 2, 1, 0}; - set_sort_idx(st2, 1, false); - set_sort_desc(st2, false); - auto r2 = compute_visible_rows(cells, 4, 2, st2); - check(r2.size() == 4 && r2[0] == 0 && r2[3] == 3, - "col_order no afecta semantica sort/filter"); - - // --- reorder_column: drag DERECHA (si [1,2,0,3] - check(s.col_order.size() == 4 && - s.col_order[0] == 1 && s.col_order[1] == 2 && - s.col_order[2] == 0 && s.col_order[3] == 3, - "reorder derecha 0->2 = [1,2,0,3]"); - } - // --- reorder_column: drag IZQUIERDA (si>di) --- - { - State s; s.col_order = {0, 1, 2, 3}; - reorder_column(s, 3, 1); - // Esperado: 3 va a la posicion donde estaba 1 -> [0,3,1,2] - check(s.col_order.size() == 4 && - s.col_order[0] == 0 && s.col_order[1] == 3 && - s.col_order[2] == 1 && s.col_order[3] == 2, - "reorder izquierda 3->1 = [0,3,1,2]"); - } - // --- reorder_column: adyacente derecha --- - { - State s; s.col_order = {0, 1, 2, 3}; - reorder_column(s, 1, 2); - // 1->2: [0,2,1,3] - check(s.col_order[0] == 0 && s.col_order[1] == 2 && - s.col_order[2] == 1 && s.col_order[3] == 3, - "reorder adyacente derecha 1->2"); - } - // --- reorder_column: no-op src==dst --- - { - State s; s.col_order = {0, 1, 2, 3}; - reorder_column(s, 2, 2); - check(s.col_order[0] == 0 && s.col_order[1] == 1 && - s.col_order[2] == 2 && s.col_order[3] == 3, - "reorder no-op src==dst"); - } - // --- reorder_column: src o dst fuera del array --- - { - State s; s.col_order = {0, 1, 2}; - reorder_column(s, 99, 0); - check(s.col_order[0] == 0 && s.col_order[1] == 1 && s.col_order[2] == 2, - "reorder src fuera de rango = no-op"); - } - - // --- tipos mixtos: int / float / bool / date --- - const char* mixed[] = { - "alpha", "1", "1.2", "true", "2025-01-15", - "beta", "2", "0.9", "false", "2025-06-01", - "gamma", "10","0.45", "true", "2024-12-31", - }; - { - State s; s.raw().filters.push_back({1, Op::Gt, "2"}); // int col1 > 2 (numerico: 10>2) - auto r = compute_visible_rows(mixed, 3, 5, s); - check(r.size() == 1 && r[0] == 2, "filtro int numerico col1 > 2"); - } - { - State s; s.raw().filters.push_back({2, Op::Lt, "1.0"}); // float col2 < 1.0 - auto r = compute_visible_rows(mixed, 3, 5, s); - check(r.size() == 2 && r[0] == 1 && r[1] == 2, "filtro float col2 < 1.0"); - } - { - State s; s.raw().filters.push_back({3, Op::Eq, "true"}); // bool col3 == true - auto r = compute_visible_rows(mixed, 3, 5, s); - check(r.size() == 2 && r[0] == 0 && r[1] == 2, "filtro bool col3 == true"); - } - { - State s; s.raw().filters.push_back({4, Op::Gte, "2025-01-01"}); // date col4 >= 2025-01-01 (lexical) - auto r = compute_visible_rows(mixed, 3, 5, s); - check(r.size() == 2 && r[0] == 0 && r[1] == 1, "filtro date col4 >= 2025-01-01"); - } - { - State s; set_sort_idx(s, 2, false); set_sort_desc(s, true); // sort float desc - auto r = compute_visible_rows(mixed, 3, 5, s); - check(r.size() == 3 && r[0] == 0 && r[1] == 1 && r[2] == 2, - "sort float desc"); - } - { - State s; set_sort_idx(s, 4, false); set_sort_desc(s, false); // sort date asc (lexical) - auto r = compute_visible_rows(mixed, 3, 5, s); - check(r.size() == 3 && r[0] == 2 && r[1] == 0 && r[2] == 1, - "sort date asc cronologico"); - } - - // --- compute_column_stats --- - { - // Col numerica con un vacio - const char* m[] = { - "1", - "2", - "", - "5", - "5", - }; - // 5 rows x 1 col - const char* m_flat[] = {"1","2","","5","5"}; - auto s = compute_column_stats(m_flat, 5, 1, 0); - check(s.total == 5 && s.empty_count == 1, "stats: total + empty_count"); - check(s.numeric == true && s.numeric_count == 4, "stats: numeric flag + count"); - check(s.min == 1.0 && s.max == 5.0, "stats: min/max numerico"); - check(s.sum == 13.0, "stats: sum"); - check(s.mean == 13.0/4.0, "stats: mean ignora vacios"); - check(s.unique_count == 3, "stats: unique 3 (1,2,5)"); - } - { - // Col mixta: parsea como string (no numeric) - const char* m[] = {"go","py","go","cpp"}; - auto s = compute_column_stats(m, 4, 1, 0); - check(s.numeric == false, "stats: lexical no es numeric"); - check(s.unique_count == 3, "stats: unique 3 (go,py,cpp)"); - check(s.empty_count == 0, "stats: sin empties"); - } - { - // Cap de uniques - const char* m[] = {"a","b","c","d","e"}; - auto s = compute_column_stats(m, 5, 1, 0, /*unique_cap=*/2); - check(s.unique_capped == true, "stats: unique_capped flag"); - check(s.unique_count <= 2, "stats: unique respeta cap"); - } - { - // Bool col - const char* m[] = {"true","false","true","true"}; - auto s = compute_column_stats(m, 4, 1, 0); - check(s.numeric == false, "stats: bool no es numeric"); - check(s.unique_count == 2, "stats: bool unique = 2"); - } - { - // Col fuera de rango - const char* m[] = {"x"}; - auto s = compute_column_stats(m, 1, 1, 99); - check(s.total == 0, "stats: col fuera de rango devuelve vacio"); - } - { - // Percentiles sobre {1..9} - const char* m[] = {"1","2","3","4","5","6","7","8","9"}; - auto s = compute_column_stats(m, 9, 1, 0); - check(s.numeric && s.numeric_count == 9, "stats: 9 nums"); - check(s.p25 == 3.0, "stats: p25 = 3"); - check(s.p50 == 5.0, "stats: p50 = 5 (mediana)"); - check(s.p75 == 7.0, "stats: p75 = 7"); - check((int)s.hist.size() == HIST_BINS, "stats: hist tiene HIST_BINS bins"); - float sum = 0.f; for (float x : s.hist) sum += x; - check((int)sum == 9, "stats: hist suma = numeric_count"); - } - { - // Histograma con todos iguales -> bin central tiene todo - const char* m[] = {"5","5","5","5"}; - auto s = compute_column_stats(m, 4, 1, 0); - check(s.min == 5.0 && s.max == 5.0, "stats: min==max homogeneo"); - check(s.hist[HIST_BINS / 2] == 4.0f, "stats: hist degenerado pone todo en bin central"); - } - { - // Stats con indices: SOLO filas indicadas se contabilizan. - const char* m_flat[] = {"1","2","3","4","5","6","7","8","9"}; - int indices[] = {0, 2, 4}; // valores 1, 3, 5 - auto s = compute_column_stats(m_flat, 9, 1, 0, 100000, indices, 3); - check(s.total == 3, "stats(idx): total = n_indices"); - check(s.numeric_count == 3, "stats(idx): numeric_count"); - check(s.min == 1.0 && s.max == 5.0, "stats(idx): min/max sobre subset"); - check(s.mean == 3.0, "stats(idx): mean = 3"); - check(s.p50 == 3.0, "stats(idx): mediana subset"); - check(s.unique_count == 3, "stats(idx): unique subset"); - } - { - // Stats reactivo a filtro: compute con visible_rows tras filtrar - const char* m_flat[] = {"a","1", "b","2", "a","3", "b","4"}; - State st; - st.raw().filters.push_back({0, Op::Eq, "a"}); - auto vis = compute_visible_rows(m_flat, 4, 2, st); - // valores col 1 filtrados: rows 0,2 -> "1","3" - auto s = compute_column_stats(m_flat, 4, 2, 1, 100000, - vis.data(), (int)vis.size()); - check(s.total == 2, "stats reactivo: total = 2 tras filter"); - check(s.numeric_count == 2, "stats reactivo: numeric_count"); - check(s.min == 1.0, "stats reactivo: min sobre subset filtrado"); - check(s.max == 3.0, "stats reactivo: max sobre subset filtrado"); - check(s.mean == 2.0, "stats reactivo: mean sobre subset filtrado"); - } - { - // Indices vacios = scan completo (n_indices=0 hace fallback) - const char* m[] = {"1","2","3"}; - auto s = compute_column_stats(m, 3, 1, 0, 100000, nullptr, 0); - check(s.total == 3, "stats: indices null -> scan completo"); - } - - // --- Ops nuevas: Contains / NotContains / StartsWith / EndsWith --- - check( compare("hello_world", "world", Op::Contains), "contains hello_world has world"); - check(!compare("hello", "xxx", Op::Contains), "!contains hello/xxx"); - check( compare("hello", "xxx", Op::NotContains), "notcontains hello/xxx"); - check(!compare("hello_world", "world", Op::NotContains), "!notcontains hello_world/world"); - check( compare("hello_world", "hello", Op::StartsWith), "starts hello_world/hello"); - check(!compare("hello_world", "world", Op::StartsWith), "!starts hello_world/world"); - check( compare("hello_world", "world", Op::EndsWith), "ends hello_world/world"); - check(!compare("hello_world", "hello", Op::EndsWith), "!ends hello_world/hello"); - check( compare("a", "", Op::Contains), "contains empty needle = true"); - check(!compare("a", "", Op::NotContains), "notcontains empty needle = false"); - check( compare("anything", "", Op::StartsWith), "starts empty prefix = true"); - check( compare("anything", "", Op::EndsWith), "ends empty suffix = true"); - check(!compare("ab", "abcd", Op::StartsWith), "starts needle longer than hay = false"); - check(!compare("ab", "abcd", Op::EndsWith), "ends needle longer than hay = false"); - check(op_is_string_only(Op::Contains) && op_is_string_only(Op::NotContains), - "op_is_string_only contains/notcontains"); - check(op_is_string_only(Op::StartsWith) && op_is_string_only(Op::EndsWith), - "op_is_string_only starts/ends"); - check(!op_is_string_only(Op::Eq) && !op_is_string_only(Op::Gt), - "op_is_string_only false para = y >"); - - // --- Filtros nuevos integrados con compute_visible_rows --- - { - const char* m[] = { - "fn_alpha", "go", - "fn_beta", "py", - "fn_gamma", "go", - "lib_x", "cpp", - }; - State st; st.raw().filters.push_back({0, Op::StartsWith, "fn_"}); - auto r = compute_visible_rows(m, 4, 2, st); - check(r.size() == 3, "filter starts_with fn_"); - st.raw().filters.clear(); - st.raw().filters.push_back({0, Op::EndsWith, "alpha"}); - r = compute_visible_rows(m, 4, 2, st); - check(r.size() == 1 && r[0] == 0, "filter ends_with alpha"); - st.raw().filters.clear(); - st.raw().filters.push_back({0, Op::Contains, "lib"}); - r = compute_visible_rows(m, 4, 2, st); - check(r.size() == 1 && r[0] == 3, "filter contains lib"); - st.raw().filters.clear(); - st.raw().filters.push_back({1, Op::NotContains, "p"}); - r = compute_visible_rows(m, 4, 2, st); - // p contiene a "py" y "cpp"; quedan rows con lang="go" (0, 2) - check(r.size() == 2 && r[0] == 0 && r[1] == 2, "filter notcontains p"); - } - - // --- Range filter como 2 filtros encadenados --- - { - const char* m[] = {"1","2","3","4","5","6","7","8","9","10"}; - State st; - st.raw().filters.push_back({0, Op::Gte, "3"}); - st.raw().filters.push_back({0, Op::Lte, "7"}); - auto r = compute_visible_rows(m, 10, 1, st); - check(r.size() == 5 && r[0] == 2 && r[4] == 6, "range [3..7] AND chain"); - } - - // --- top_categories --- - { - const char* m[] = {"go","py","go","cpp","go","py","cpp","cpp","go"}; - auto s = compute_column_stats(m, 9, 1, 0); - check(s.top_categories.size() == 3, "top_categories size = 3 distintos"); - // go=4, cpp=3, py=2 - check(s.top_categories[0].first == "go" && s.top_categories[0].second == 4, - "top_categories[0] = go,4"); - check(s.top_categories[1].first == "cpp" && s.top_categories[1].second == 3, - "top_categories[1] = cpp,3"); - check(s.top_categories[2].first == "py" && s.top_categories[2].second == 2, - "top_categories[2] = py,2"); - } - - // --- csv_escape --- - check(csv_escape("simple") == "simple", "csv_escape: sin caracteres especiales"); - check(csv_escape("a,b") == "\"a,b\"", "csv_escape: coma -> quotes"); - check(csv_escape("a\"b") == "\"a\"\"b\"", "csv_escape: quote doblada"); - check(csv_escape("a\nb") == "\"a\nb\"", "csv_escape: newline -> quotes"); - check(csv_escape(nullptr) == "", "csv_escape: null -> empty"); - - // --- build_tsv: rect selection con headers --- - { - const char* cells_t[] = { - "1","a","X", - "2","b","Y", - "3","c","Z", - }; - const char* headers_t[] = {"num","letter","tag"}; - std::vector col_order = {0, 1, 2}; - std::vector col_vis = {true, true, true}; - std::vector visible = {0, 1, 2}; - // Selecciona rect (rows 0..1, cols 1..2) -> letter+tag, rows a,X / b,Y - auto tsv = build_tsv(cells_t, 3, 3, headers_t, col_order, col_vis, visible, - 0, 1, 1, 2); - std::string expected = "letter\ttag\na\tX\nb\tY\n"; - check(tsv == expected, "build_tsv rect 0..1 x 1..2 + headers"); - } - { - // build_tsv con columna oculta dentro del rect -> se omite - const char* cells_t[] = {"1","a","X","2","b","Y"}; - const char* headers_t[] = {"num","letter","tag"}; - std::vector col_order = {0, 1, 2}; - std::vector col_vis = {true, false, true}; // letter oculto - std::vector visible = {0, 1}; - auto tsv = build_tsv(cells_t, 2, 3, headers_t, col_order, col_vis, visible, - 0, 1, 0, 2); - std::string expected = "num\ttag\n1\tX\n2\tY\n"; - check(tsv == expected, "build_tsv salta columna oculta"); - } - { - // build_tsv respeta col_order custom - const char* cells_t[] = {"1","a","2","b"}; - const char* headers_t[] = {"num","letter"}; - std::vector col_order = {1, 0}; // letter primero - std::vector col_vis = {true, true}; - std::vector visible = {0, 1}; - auto tsv = build_tsv(cells_t, 2, 2, headers_t, col_order, col_vis, visible, - 0, 1, 0, 1); - std::string expected = "letter\tnum\na\t1\nb\t2\n"; - check(tsv == expected, "build_tsv respeta col_order reordenado"); - } - - // --- build_csv: full filtered view con escape --- - { - const char* cells_c[] = { - "x", "1", - "y,z", "2", - "w\"q","3", - }; - const char* headers_c[] = {"name","n"}; - std::vector col_order = {0, 1}; - std::vector col_vis = {true, true}; - std::vector visible = {0, 1, 2}; - auto csv = build_csv(cells_c, 3, 2, headers_c, col_order, col_vis, visible); - std::string expected = "name,n\nx,1\n\"y,z\",2\n\"w\"\"q\",3\n"; - check(csv == expected, "build_csv con escape de coma y quote"); - } - { - // build_csv vacio si no hay rows visibles - const char* cells_c[] = {"x","1"}; - const char* headers_c[] = {"name","n"}; - std::vector col_order = {0, 1}; - std::vector col_vis = {true, true}; - std::vector visible; // ninguna fila visible - auto csv = build_csv(cells_c, 1, 2, headers_c, col_order, col_vis, visible); - check(csv == "name,n\n", "build_csv solo headers si filter vacia rows"); - } - - // --- ColumnType: auto_detect_type --- - { - const char* m[] = {"1","2","3","4"}; - check(auto_detect_type(m, 4, 1, 0) == ColumnType::Int, "detect Int puro"); - } - { - const char* m[] = {"1","2.5","3"}; - check(auto_detect_type(m, 3, 1, 0) == ColumnType::Float, "detect Float (mix int+float)"); - } - { - const char* m[] = {"true","false","true"}; - check(auto_detect_type(m, 3, 1, 0) == ColumnType::Bool, "detect Bool"); - } - { - const char* m[] = {"2025-01-15","2025-06-30","2024-12-31"}; - check(auto_detect_type(m, 3, 1, 0) == ColumnType::Date, "detect Date ISO"); - } - { - const char* m[] = {"{\"k\":1}","[1,2,3]","{}"}; - check(auto_detect_type(m, 3, 1, 0) == ColumnType::Json, "detect Json"); - } - { - const char* m[] = {"hello","world","foo"}; - check(auto_detect_type(m, 3, 1, 0) == ColumnType::String, "detect String"); - } - { - const char* m[] = {"1","hello","2"}; - check(auto_detect_type(m, 3, 1, 0) == ColumnType::String, "mix int+string -> String"); - } - { - const char* m[] = {"true","yes","false"}; // 'yes' no es bool literal estricto - check(auto_detect_type(m, 3, 1, 0) == ColumnType::String, "bool laxo -> String"); - } - { - const char* m[] = {"","",""}; // todo vacio - check(auto_detect_type(m, 3, 1, 0) == ColumnType::String, "todo vacio -> String"); - } - - // --- ops_for_type --- - { - auto o = ops_for_type(ColumnType::Int); - check(o.size() == 6, "ops Int = 6"); - bool has_gt = false; for (Op x : o) if (x == Op::Gt) has_gt = true; - check(has_gt, "ops Int incluye >"); - } - { - auto o = ops_for_type(ColumnType::Float); - check(o.size() == 6, "ops Float = 6"); - } - { - auto o = ops_for_type(ColumnType::Date); - check(o.size() == 6, "ops Date = 6 (lexical = cronologico)"); - } - { - auto o = ops_for_type(ColumnType::Bool); - check(o.size() == 2, "ops Bool = 2 (= y !=)"); - check(o[0] == Op::Eq && o[1] == Op::Neq, "ops Bool [Eq, Neq]"); - } - { - auto o = ops_for_type(ColumnType::Json); - check(o.size() == 4, "ops Json = 4"); - bool has_contains = false; for (Op x : o) if (x == Op::Contains) has_contains = true; - check(has_contains, "ops Json incluye contains"); - } - { - auto o = ops_for_type(ColumnType::String); - check(o.size() == 6, "ops String = 6"); - bool has_starts = false; for (Op x : o) if (x == Op::StartsWith) has_starts = true; - check(has_starts, "ops String incluye starts"); - } - - // --- effective_type --- - { - const char* m[] = {"1","2","3"}; - check(effective_type(ColumnType::Bool, m, 3, 1, 0) == ColumnType::Bool, - "effective: declared Bool gana sobre datos numericos"); - check(effective_type(ColumnType::Auto, m, 3, 1, 0) == ColumnType::Int, - "effective: Auto resuelve a Int via auto_detect"); - } - - // --- lua_engine: compile + eval + sandbox --- - { - auto* eng = lua_engine::get(); - const char* cells_lua[] = { - "alpha", "10", - "beta", "20", - "gamma", "30", - }; - std::vector hn = {"name", "qty"}; - std::unordered_map n2c = {{"name", 0}, {"qty", 1}}; - auto mk_ctx = [&](int r){ - lua_engine::RowCtx ctx; - ctx.cells = cells_lua; - ctx.orig_cols = 2; - ctx.row = r; - ctx.header_names = &hn; - ctx.name_to_col = &n2c; - return ctx; - }; - - std::string err; - int id = lua_engine::compile(eng, "return row.qty * 2", &err); - check(id >= 0, "lua: compile arithmetic OK"); - check(lua_engine::eval(eng, id, mk_ctx(0), &err) == "20", "lua: eval 10*2 = 20"); - check(lua_engine::eval(eng, id, mk_ctx(2), &err) == "60", "lua: eval 30*2 = 60"); - lua_engine::release(eng, id); - - id = lua_engine::compile(eng, "return fn.upper(row.name)", &err); - check(id >= 0, "lua: compile builtin OK"); - check(lua_engine::eval(eng, id, mk_ctx(0), &err) == "ALPHA", "lua: fn.upper"); - lua_engine::release(eng, id); - - id = lua_engine::compile(eng, - "if tonumber(row.qty) >= 20 then return 'high' else return 'low' end", &err); - check(id >= 0, "lua: compile if/else OK"); - check(lua_engine::eval(eng, id, mk_ctx(0), &err) == "low", "lua: if/else low"); - check(lua_engine::eval(eng, id, mk_ctx(1), &err) == "high", "lua: if/else high"); - lua_engine::release(eng, id); - - id = lua_engine::compile(eng, "return io == nil", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "true", - "lua sandbox: io is nil"); - lua_engine::release(eng, id); - id = lua_engine::compile(eng, "return require == nil", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "true", - "lua sandbox: require is nil"); - lua_engine::release(eng, id); - id = lua_engine::compile(eng, "return dofile == nil", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "true", - "lua sandbox: dofile is nil"); - lua_engine::release(eng, id); - id = lua_engine::compile(eng, "return os.execute == nil", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "true", - "lua sandbox: os.execute is nil"); - lua_engine::release(eng, id); - id = lua_engine::compile(eng, "return type(os.date) == 'function'", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "true", - "lua sandbox: os.date preservado"); - lua_engine::release(eng, id); - - err.clear(); - id = lua_engine::compile(eng, "return row.qty *", &err); - check(id == -1 && !err.empty(), "lua: error sintaxis devuelve -1 + err"); - - id = lua_engine::compile(eng, "error('boom')", &err); - check(id >= 0, "lua: compile error() OK"); - err.clear(); - std::string out = lua_engine::eval(eng, id, mk_ctx(0), &err); - check(out == "" && !err.empty(), "lua: runtime error -> '' + err"); - lua_engine::release(eng, id); - - id = lua_engine::compile(eng, "return fn.length('hello')", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "5", "lua: fn.length"); - lua_engine::release(eng, id); - id = lua_engine::compile(eng, "return fn.concat('a', '-', 'b')", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "a-b", "lua: fn.concat"); - lua_engine::release(eng, id); - id = lua_engine::compile(eng, "return fn.contains('foobar', 'oob')", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "true", "lua: fn.contains"); - lua_engine::release(eng, id); - id = lua_engine::compile(eng, "return fn.starts_with('hello_world', 'hello')", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "true", "lua: fn.starts_with"); - lua_engine::release(eng, id); - id = lua_engine::compile(eng, "return fn.year('2025-09-10')", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "2025", "lua: fn.year"); - lua_engine::release(eng, id); - id = lua_engine::compile(eng, "return fn.month('2025-09-10')", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "9", "lua: fn.month"); - lua_engine::release(eng, id); - - id = lua_engine::compile(eng, "return row[2]", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "10", "lua: row[2] = qty"); - lua_engine::release(eng, id); - id = lua_engine::compile(eng, "return row.nope == nil", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "true", - "lua: col inexistente -> nil"); - lua_engine::release(eng, id); - } - - // --- lua_engine v2: [col] preprocesser, type-aware push, recursion --- - { - auto* eng = lua_engine::get(); - // Dataset con tipos declarados - const char* cells2[] = { - "alpha", "10", "1.5", "true", "2025-01-15", - "beta", "20", "2.5", "false", "2024-06-01", - "gamma", "30", "3.5", "true", "2026-12-31", - }; - std::vector hn2 = {"name", "qty", "size", "flag", "dt"}; - std::unordered_map n2c2 = { - {"name", 0}, {"qty", 1}, {"size", 2}, {"flag", 3}, {"dt", 4} - }; - ColumnType types2[] = { - ColumnType::String, ColumnType::Int, ColumnType::Float, - ColumnType::Bool, ColumnType::Date - }; - std::vector derived; - std::unordered_map dn2i; - auto mk_ctx = [&](int r){ - lua_engine::RowCtx ctx; - ctx.cells = cells2; - ctx.orig_cols = 5; - ctx.row = r; - ctx.header_names = &hn2; - ctx.name_to_col = &n2c2; - ctx.types_orig = types2; - ctx.n_types_orig = 5; - ctx.derived = &derived; - ctx.derived_name_to_idx = &dn2i; - return ctx; - }; - - std::string err; - // [col] sintaxis basica - int id = lua_engine::compile(eng, "return [qty] + 1", &err); - check(id >= 0, "lua v2: compile [qty] + 1"); - check(lua_engine::eval(eng, id, mk_ctx(0), &err) == "11", "lua v2: [qty]+1 row0 = 11"); - lua_engine::release(eng, id); - - // Auto-return: expresion suelta sin return - id = lua_engine::compile(eng, "[qty] + [size]", &err); - check(id >= 0, "lua v2: auto-return compile"); - // Int 10 + Float 1.5 -> 11.5 - check(lua_engine::eval(eng, id, mk_ctx(0), &err) == "11.5", - "lua v2: auto-return [qty]+[size] = 11.5"); - lua_engine::release(eng, id); - - // Type-aware push: Int * 2 = integer - id = lua_engine::compile(eng, "[qty] * 2", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(1), &err) == "40", - "lua v2: Int*2 = integer (40 no 40.0)"); - lua_engine::release(eng, id); - - // Bool push: if [flag] then ... - id = lua_engine::compile(eng, "if [flag] then return 'yes' else return 'no' end", &err); - check(id >= 0, "lua v2: bool if compile"); - check(lua_engine::eval(eng, id, mk_ctx(0), &err) == "yes", "lua v2: flag=true -> yes"); - check(lua_engine::eval(eng, id, mk_ctx(1), &err) == "no", "lua v2: flag=false -> no"); - lua_engine::release(eng, id); - - // Date push: string - id = lua_engine::compile(eng, "fn.year([dt])", &err); - check(id >= 0, "lua v2: fn.year([dt]) compile"); - check(lua_engine::eval(eng, id, mk_ctx(0), &err) == "2025", "lua v2: year row0 = 2025"); - check(lua_engine::eval(eng, id, mk_ctx(2), &err) == "2026", "lua v2: year row2 = 2026"); - lua_engine::release(eng, id); - - // String concat - id = lua_engine::compile(eng, "[name] .. '-' .. [qty]", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "alpha-10", - "lua v2: string concat [name].'-'.[qty]"); - lua_engine::release(eng, id); - - // [col] dentro de string literal: NO se traduce - id = lua_engine::compile(eng, "return '[qty]'", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "[qty]", - "lua v2: string literal preserva [qty]"); - lua_engine::release(eng, id); - - // [col] dentro de comentario corto: NO se traduce - id = lua_engine::compile(eng, "-- [qty] is ignored\nreturn [qty]", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "10", - "lua v2: short comment preserva [qty]"); - lua_engine::release(eng, id); - - // [col] dentro de comentario largo: NO se traduce - id = lua_engine::compile(eng, "--[[ [qty] is here ]]\nreturn [qty]", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "10", - "lua v2: long comment preserva [qty]"); - lua_engine::release(eng, id); - - // t[1] indice numerico: NO se traduce - id = lua_engine::compile(eng, "local t = {7,8,9}\nreturn t[1]", &err); - check(id >= 0 && lua_engine::eval(eng, id, mk_ctx(0), &err) == "7", - "lua v2: indice numerico t[1] intacto"); - lua_engine::release(eng, id); - - // UTF-8 en nombre de col - std::vector hn_utf = {"año", "qty"}; - std::unordered_map n2c_utf = {{"año", 0}, {"qty", 1}}; - const char* cells_utf[] = {"2025", "10", "2026", "20"}; - ColumnType types_utf[] = {ColumnType::Int, ColumnType::Int}; - std::vector empty_d; - std::unordered_map empty_dn; - auto mk_utf = [&](int r){ - lua_engine::RowCtx c; - c.cells = cells_utf; c.orig_cols = 2; c.row = r; - c.header_names = &hn_utf; c.name_to_col = &n2c_utf; - c.types_orig = types_utf; c.n_types_orig = 2; - c.derived = &empty_d; c.derived_name_to_idx = &empty_dn; - return c; - }; - id = lua_engine::compile(eng, "[año] + 1", &err); - check(id >= 0, "lua v2: compile [año] UTF-8"); - check(lua_engine::eval(eng, id, mk_utf(0), &err) == "2026", - "lua v2: [año] UTF-8 row0 = 2026"); - lua_engine::release(eng, id); - - // Recursivo: derived A refs orig, derived B refs A - // A = "[qty] * 2" (Int) - // B = "[A] + 100" (Int) - int idA = lua_engine::compile(eng, "[qty] * 2", &err); - check(idA >= 0, "lua v2: compile derived A"); - DerivedColumn dA; dA.source_col = -1; dA.type = ColumnType::Int; - dA.name = "A"; dA.formula = "[qty] * 2"; dA.lua_id = idA; - derived.push_back(dA); - dn2i["A"] = 0; - - int idB = lua_engine::compile(eng, "[A] + 100", &err); - check(idB >= 0, "lua v2: compile derived B (refs A)"); - DerivedColumn dB; dB.source_col = -1; dB.type = ColumnType::Int; - dB.name = "B"; dB.formula = "[A] + 100"; dB.lua_id = idB; - derived.push_back(dB); - dn2i["B"] = 1; - - // row0: qty=10, A=10*2=20, B=20+100=120 - check(lua_engine::eval(eng, idA, mk_ctx(0), &err) == "20", - "lua v2: derived A = 20"); - check(lua_engine::eval(eng, idB, mk_ctx(0), &err) == "120", - "lua v2: derived B = A + 100 = 120 (recursive)"); - - // Cadena de 3 niveles: C = [B] * 2 - int idC = lua_engine::compile(eng, "[B] * 2", &err); - check(idC >= 0, "lua v2: compile derived C (refs B)"); - DerivedColumn dC; dC.source_col = -1; dC.type = ColumnType::Int; - dC.name = "C"; dC.formula = "[B] * 2"; dC.lua_id = idC; - derived.push_back(dC); - dn2i["C"] = 2; - check(lua_engine::eval(eng, idC, mk_ctx(0), &err) == "240", - "lua v2: chain C -> B -> A -> qty = 240"); - - // Ciclo: D refs E, E refs D -> nil propaga - int idD = lua_engine::compile(eng, "[E] + 1", &err); - check(idD >= 0, "lua v2: compile D (refs E)"); - DerivedColumn dD; dD.source_col=-1; dD.type=ColumnType::Int; - dD.name="D"; dD.formula="[E]+1"; dD.lua_id=idD; - derived.push_back(dD); dn2i["D"] = 3; - - int idE = lua_engine::compile(eng, "[D] + 1", &err); - check(idE >= 0, "lua v2: compile E (refs D)"); - DerivedColumn dE; dE.source_col=-1; dE.type=ColumnType::Int; - dE.name="E"; dE.formula="[D]+1"; dE.lua_id=idE; - derived.push_back(dE); dn2i["E"] = 4; - - // Evaluar D debe romper el ciclo: [E] devuelve nil, nil+1 error, - // pcall captura -> eval devuelve "" + err - err.clear(); - std::string r = lua_engine::eval(eng, idD, mk_ctx(0), &err); - check(r.empty(), "lua v2: ciclo D<->E devuelve vacio sin crash"); - - lua_engine::release(eng, idA); - lua_engine::release(eng, idB); - lua_engine::release(eng, idC); - lua_engine::release(eng, idD); - lua_engine::release(eng, idE); - derived.clear(); - dn2i.clear(); - - // Retipo puro (sin formula) accesible via row. - derived.push_back({0, ColumnType::String, "name_str", "", -1, ""}); // source_col=0 (name) - dn2i["name_str"] = 0; - int idF = lua_engine::compile(eng, "[name_str] .. '_X'", &err); - check(idF >= 0, "lua v2: compile usando retipo puro"); - check(lua_engine::eval(eng, idF, mk_ctx(0), &err) == "alpha_X", - "lua v2: row[retipo_puro] funciona"); - lua_engine::release(eng, idF); - } - - // --- autocomplete helpers: find_open_bracket + insert_column_ref --- - { - std::string ft; - // Cursor justo despues de "[" - int idx = find_open_bracket("foo [", 5, 5, ft); - check(idx == 4 && ft == "", "ac: find_open_bracket cursor tras ["); - idx = find_open_bracket("foo [abc", 8, 8, ft); - check(idx == 4 && ft == "abc", "ac: filter 'abc' tras ["); - idx = find_open_bracket("foo [a] + 1", 11, 11, ft); - check(idx == -1, "ac: bracket cerrado -> -1"); - idx = find_open_bracket("foo [a\nbar", 10, 10, ft); - check(idx == -1, "ac: newline interrumpe"); - idx = find_open_bracket("nada", 4, 4, ft); - check(idx == -1, "ac: sin bracket -> -1"); - idx = find_open_bracket("[xy", 3, 3, ft); - check(idx == 0 && ft == "xy", "ac: bracket al inicio"); - idx = find_open_bracket("a [b] + [c", 10, 10, ft); - check(idx == 8 && ft == "c", "ac: segundo bracket abierto"); - } - { - int nc = 0; - std::string r = insert_column_ref("foo [", 4, 5, "size_kb", nc); - check(r == "foo [size_kb]" && nc == 13, "ac: insert tras [ -> [size_kb]"); - r = insert_column_ref("foo [ab", 4, 7, "size_kb", nc); - check(r == "foo [size_kb]" && nc == 13, "ac: reemplaza filter tecleado"); - r = insert_column_ref("[a] + [", 6, 7, "qty", nc); - check(r == "[a] + [qty]" && nc == 11, "ac: insert preserva prefijo"); - r = insert_column_ref("[a", 0, 2, "name", nc); - check(r == "[name]" && nc == 6, "ac: reemplaza [a -> [name]"); - // Edge: start fuera de rango - r = insert_column_ref("hi", -1, 1, "n", nc); - check(r == "hi", "ac: start invalido = no-op"); - r = insert_column_ref("hi", 0, 99, "n", nc); - check(r == "hi", "ac: cursor invalido = no-op"); - } - - // --- preprocess() expuesto: brackets + auto-return --- - { - check(lua_engine::preprocess("[a] + [b]") == "return row[\"a\"] + row[\"b\"]", - "preprocess: [a]+[b] -> return row[\"a\"] + row[\"b\"]"); - check(lua_engine::preprocess("return [a]") == "return row[\"a\"]", - "preprocess: con return explicito no duplica"); - check(lua_engine::preprocess("if [a] then return 1 end") - == "if row[\"a\"] then return 1 end", - "preprocess: if no añade return"); - check(lua_engine::preprocess("'[a]'") == "return '[a]'", - "preprocess: string literal preserva [a]"); - check(lua_engine::preprocess("-- [a]\nreturn 1") - == "-- [a]\nreturn 1", - "preprocess: short comment preserva [a]"); - check(lua_engine::preprocess("[a b]") == "return row[\"a b\"]", - "preprocess: nombre con espacio"); - } - - // --- TQL: aggregation_alias + aggregation_type --- - { - check(aggregation_alias({AggFn::Count}) == "count", "tql alias count"); - check(aggregation_alias({AggFn::Avg, "size_kb"}) == "avg_size_kb", "tql alias avg_size_kb"); - check(aggregation_alias({AggFn::Distinct, "name"}) == "distinct_name", "tql alias distinct_name"); - Aggregation p95; p95.fn = AggFn::Percentile; p95.col = "size_kb"; p95.arg = 0.95; - check(aggregation_alias(p95) == "p95_size_kb", "tql alias p95_size_kb"); - Aggregation aliased; aliased.fn = AggFn::Sum; aliased.col = "x"; aliased.alias = "total"; - check(aggregation_alias(aliased) == "total", "tql alias usa alias explicito"); - - std::vector hdrs = {"lang", "size_kb", "name"}; - std::vector tps = {ColumnType::String, ColumnType::Float, ColumnType::String}; - check(aggregation_type({AggFn::Count}, hdrs, tps) == ColumnType::Int, "tql type count = Int"); - check(aggregation_type({AggFn::Distinct, "name"}, hdrs, tps) == ColumnType::Int, "tql type distinct = Int"); - check(aggregation_type({AggFn::Avg, "size_kb"}, hdrs, tps) == ColumnType::Float, "tql type avg = Float"); - check(aggregation_type({AggFn::Min, "name"}, hdrs, tps) == ColumnType::String, "tql type min(string) = String"); - check(aggregation_type({AggFn::Min, "size_kb"}, hdrs, tps) == ColumnType::Float, "tql type min(float) = Float"); - } - - // --- TQL: compute_stage passthrough (filter + sort sin group) --- - { - const char* cells_t[] = { - "go", "10", - "py", "20", - "go", "30", - "cpp", "5", - }; - std::vector hdrs = {"lang", "n"}; - std::vector tps = {ColumnType::String, ColumnType::Int}; - Stage s; - s.filters.push_back({0, Op::Eq, "go"}); - s.sorts.push_back({"n", true}); - auto out = compute_stage(cells_t, 4, 2, hdrs, tps, s); - check(out.rows == 2 && out.cols == 2, "tql passthrough rows + cols"); - check(std::string(out.cells[0]) == "go" && std::string(out.cells[1]) == "30", - "tql passthrough sort desc por n: 30 primero"); - check(std::string(out.cells[2]) == "go" && std::string(out.cells[3]) == "10", - "tql passthrough sort desc: 10 segundo"); - } - - // --- TQL: compute_stage group by 1 col + count --- - { - const char* cells_t[] = { - "go", "10", - "py", "20", - "go", "30", - "cpp", "5", - "go", "15", - "py", "25", - }; - std::vector hdrs = {"lang", "n"}; - std::vector tps = {ColumnType::String, ColumnType::Int}; - Stage s; - s.breakouts.push_back("lang"); - s.aggregations.push_back({AggFn::Count}); - s.aggregations.push_back({AggFn::Avg, "n"}); - s.aggregations.push_back({AggFn::Sum, "n"}); - s.sorts.push_back({"count", true}); - auto out = compute_stage(cells_t, 6, 2, hdrs, tps, s); - check(out.cols == 4, "tql group: cols = breakouts + aggs"); - check(out.rows == 3, "tql group: 3 grupos (go/py/cpp)"); - // headers - check(out.headers[0] == "lang" && out.headers[1] == "count" && - out.headers[2] == "avg_n" && out.headers[3] == "sum_n", - "tql group: headers correctos"); - // sort desc por count -> go (3) primero, py (2) segundo, cpp (1) ultimo - check(std::string(out.cells[0*4+0]) == "go" && - std::string(out.cells[0*4+1]) == "3", - "tql group row0: lang=go count=3"); - check(std::string(out.cells[1*4+0]) == "py" && - std::string(out.cells[1*4+1]) == "2", - "tql group row1: lang=py count=2"); - // avg de go: (10+30+15)/3 = 18.33 (formatear como %.4g = "18.33") - // sum de go: 55 - check(std::string(out.cells[0*4+2]).find("18.33") != std::string::npos, - "tql group: avg_n go ~ 18.33"); - check(std::string(out.cells[0*4+3]) == "55", "tql group: sum_n go = 55"); - } - - // --- TQL: compute_stage 2 breakouts + multiple aggs --- - { - const char* cells_t[] = { - "go", "core", "10", - "go", "infra", "20", - "py", "core", "30", - "go", "core", "40", - "py", "infra", "50", - "py", "core", "60", - }; - std::vector hdrs = {"lang", "domain", "n"}; - std::vector tps = {ColumnType::String, ColumnType::String, ColumnType::Int}; - Stage s; - s.breakouts.push_back("lang"); - s.breakouts.push_back("domain"); - s.aggregations.push_back({AggFn::Count}); - s.aggregations.push_back({AggFn::Min, "n"}); - s.aggregations.push_back({AggFn::Max, "n"}); - auto out = compute_stage(cells_t, 6, 3, hdrs, tps, s); - check(out.rows == 4, "tql 2 breakouts: 4 grupos (go/core, go/infra, py/core, py/infra)"); - check(out.cols == 5, "tql 2 breakouts: 5 cols"); - } - - // --- TQL: percentile + median + stddev --- - { - const char* cells_t[] = { - "a", "1", - "a", "2", - "a", "3", - "a", "4", - "a", "5", - "a", "6", - "a", "7", - "a", "8", - "a", "9", - }; - std::vector hdrs = {"k", "n"}; - std::vector tps = {ColumnType::String, ColumnType::Int}; - Stage s; - s.breakouts.push_back("k"); - s.aggregations.push_back({AggFn::Median, "n"}); - s.aggregations.push_back({AggFn::P25, "n"}); - s.aggregations.push_back({AggFn::P75, "n"}); - Aggregation p90; p90.fn = AggFn::P90; p90.col = "n"; - s.aggregations.push_back(p90); - Aggregation pct; pct.fn = AggFn::Percentile; pct.col = "n"; pct.arg = 0.5; - s.aggregations.push_back(pct); - s.aggregations.push_back({AggFn::Stddev, "n"}); - auto out = compute_stage(cells_t, 9, 2, hdrs, tps, s); - check(out.rows == 1, "tql percentiles: 1 grupo"); - // headers: k, median_n, p25_n, p75_n, p90_n, p50_n, stddev_n - check(out.headers[1] == "median_n", "tql median alias"); - check(out.headers[2] == "p25_n", "tql p25 alias"); - check(out.headers[4] == "p90_n", "tql p90 alias"); - check(out.headers[5] == "p50_n", "tql percentile generico -> p50_n"); - check(out.headers[6] == "stddev_n", "tql stddev alias"); - // median = 5 - check(std::string(out.cells[1]) == "5", "tql median(1..9) = 5"); - // p25 = 3, p75 = 7 - check(std::string(out.cells[2]) == "3", "tql p25(1..9) = 3"); - check(std::string(out.cells[3]) == "7", "tql p75(1..9) = 7"); - } - - // --- TQL: distinct counts --- - { - const char* cells_t[] = { - "go", "filter", - "go", "map", - "go", "filter", - "py", "sma", - "py", "sma", - "py", "ema", - }; - std::vector hdrs = {"lang", "name"}; - std::vector tps = {ColumnType::String, ColumnType::String}; - Stage s; - s.breakouts.push_back("lang"); - s.aggregations.push_back({AggFn::Distinct, "name"}); - auto out = compute_stage(cells_t, 6, 2, hdrs, tps, s); - check(out.rows == 2, "tql distinct: 2 grupos"); - // go: distinct {filter, map} = 2 - // py: distinct {sma, ema} = 2 - for (int r = 0; r < 2; ++r) { - check(std::string(out.cells[r * 2 + 1]) == "2", - "tql distinct cuenta unicos"); - } - } - - // --- TQL: stage chain (output of stage 0 feeds stage 1) --- - { - // Stage 0: filter lang=go -> passthrough. - // Stage 1: group by domain, count + avg n. - const char* cells_t[] = { - "go", "core", "10", - "go", "infra", "20", - "py", "core", "30", - "go", "core", "40", - }; - std::vector hdrs = {"lang", "domain", "n"}; - std::vector tps = {ColumnType::String, ColumnType::String, ColumnType::Int}; - Stage s0; - s0.filters.push_back({0, Op::Eq, "go"}); - auto out0 = compute_stage(cells_t, 4, 3, hdrs, tps, s0); - check(out0.rows == 3, "tql chain stage0: filtra a 3 filas"); - - Stage s1; - s1.breakouts.push_back("domain"); - s1.aggregations.push_back({AggFn::Count}); - s1.aggregations.push_back({AggFn::Avg, "n"}); - auto out1 = compute_stage(out0.cells.data(), out0.rows, out0.cols, - out0.headers, out0.types, s1); - check(out1.rows == 2, "tql chain stage1: 2 grupos (core/infra)"); - check(out1.headers[0] == "domain" && out1.headers[1] == "count" && - out1.headers[2] == "avg_n", - "tql chain stage1: headers"); - } - - // --- TQL emit --- - { - State st; - std::vector hdrs = {"lang", "n", "name"}; - // Empty state -> minimal - std::vector tps = {ColumnType::String, ColumnType::Int, ColumnType::String}; - std::string out = tql::emit(st, hdrs, tps); - check(out.find("stages") != std::string::npos, "tql emit: contiene stages"); - - // Con filters + sort - st.raw().filters.push_back({0, Op::Eq, "go"}); - st.raw().filters.push_back({1, Op::Gte, "10"}); - set_sort_idx(st, 1, false); - set_sort_desc(st, true); - out = tql::emit(st, hdrs, tps); - check(out.find("filter") != std::string::npos, "tql emit: incluye filter"); - check(out.find("\"=\"") != std::string::npos, "tql emit: op ="); - check(out.find("\"lang\"") != std::string::npos, "tql emit: col lang"); - check(out.find("\"go\"") != std::string::npos, "tql emit: value go"); - check(out.find("\">=\"") != std::string::npos, "tql emit: op >="); - check(out.find("sort") != std::string::npos, "tql emit: incluye sort"); - check(out.find("\"desc\"") != std::string::npos, "tql emit: sort dir desc"); - } - - // --- TQL apply --- - { - State st; - std::vector hdrs = {"lang", "n", "name"}; - const char* cells_t[] = { - "go", "10", "filter", - "py", "20", "sma", - "go", "30", "map", - }; - std::string text = R"LUA( -return { - stages = { - { - filter = { - {"=", "lang", "go"}, - {">=", "n", "10"}, - }, - sort = { {"desc", "n"} }, - } - } -})LUA"; - std::string err; - bool ok = tql::apply(text, st, hdrs, std::vector{}, cells_t, 3, 3, &err); - check(ok, "tql apply: parsea OK"); - check(st.raw().filters.size() == 2, "tql apply: 2 filters"); - check(st.raw().filters[0].col == 0 && st.raw().filters[0].op == Op::Eq && - st.raw().filters[0].value == "go", "tql apply: filter 0 = lang=go"); - check(st.raw().filters[1].col == 1 && st.raw().filters[1].op == Op::Gte && - st.raw().filters[1].value == "10", "tql apply: filter 1 = n>=10"); - // sort se almacena por nombre en el nuevo modelo (no por indice). - check(st.raw().sorts.size() == 1 && st.raw().sorts[0].col == "n" && - st.raw().sorts[0].desc == true, - "tql apply: sort desc por n (by name)"); - } - - // --- TQL apply error: invalid Lua --- - { - State st; - std::vector hdrs = {"a"}; - std::string err; - bool ok = tql::apply("return {{{ not valid lua", st, hdrs, std::vector{}, nullptr, 0, 1, &err); - check(!ok && !err.empty(), "tql apply: lua invalido -> false + err"); - } - - // --- TQL apply error: root no es tabla --- - { - State st; - std::vector hdrs = {"a"}; - std::string err; - bool ok = tql::apply("return 42", st, hdrs, std::vector{}, nullptr, 0, 1, &err); - check(!ok && err.find("table") != std::string::npos, - "tql apply: root no-tabla -> error"); - } - - // --- TQL round-trip: emit -> apply -> compare --- - { - State st0; - std::vector hdrs = {"lang", "n"}; - st0.raw().filters.push_back({0, Op::Contains, "g"}); - st0.raw().filters.push_back({1, Op::Lt, "100"}); - set_sort_idx(st0, 0, false); - set_sort_desc(st0, false); - - std::vector tps_rt = {ColumnType::String, ColumnType::Int}; - std::string text = tql::emit(st0, hdrs, tps_rt); - - State st1; - const char* cells_t[] = {"go","1","py","2"}; - std::string err; - bool ok = tql::apply(text, st1, hdrs, tps_rt, cells_t, 2, 2, &err); - check(ok, "tql round-trip: apply OK"); - check(st1.raw().filters.size() == 2, "tql round-trip: 2 filters preservados"); - check(st1.raw().filters[0].col == 0 && st1.raw().filters[0].op == Op::Contains && - st1.raw().filters[0].value == "g", - "tql round-trip: contains preservado"); - check(st1.raw().filters[1].op == Op::Lt && st1.raw().filters[1].value == "100", - "tql round-trip: < preservado"); - // En el round-trip el sort se preserva por nombre. El helper - // set_sort_idx emite con sintaxis "@N" que el round-trip respeta. - check(st1.raw().sorts.size() == 1 && st1.raw().sorts[0].desc == false, - "tql round-trip: sort asc preservado"); - } - - // --- TQL apply: expressions compila + auto-detect tipo --- - { - State st; - std::vector hdrs = {"size_kb", "name"}; - const char* cells_t[] = { - "1.5", "alpha", - "2.0", "beta", - "3.5", "gamma", - }; - std::string text = R"LUA( -return { - stages = { - { - expressions = { - size_bytes = "[size_kb] * 1024", - double_size = "[size_kb] * 2", - }, - } - } -})LUA"; - std::string err; - bool ok = tql::apply(text, st, hdrs, std::vector{}, cells_t, 3, 2, &err); - check(ok, "tql apply expressions: OK"); - check(st.raw().derived.size() == 2, "tql apply: 2 derived cols"); - // Verifica que tienen lua_id valido y formula - for (const auto& d : st.raw().derived) { - check(d.lua_id >= 0 && !d.formula.empty(), - "tql apply: derived compiled OK"); - } - } - - // --- TQL columns + color round-trip --- - { - check(tql::color_to_hex(0xFFFF0000) == "#0000ff", "tql color: blue 0xFFFF0000 -> #0000ff"); - check(tql::color_to_hex(0x80808080) == "#80808080", "tql color: con alpha"); - check(tql::hex_to_color("#0000ff") == 0xFFFF0000, "tql hex: #0000ff -> blue full alpha"); - check(tql::hex_to_color("#80808080") == 0x80808080, "tql hex: roundtrip con alpha"); - check(tql::column_type_from_string("int") == ColumnType::Int, "tql ctype: int"); - check(tql::column_type_from_string("bool") == ColumnType::Bool, "tql ctype: bool"); - check(tql::column_type_from_string("date") == ColumnType::Date, "tql ctype: date"); - check(tql::column_type_from_string("zzz") == ColumnType::Auto, "tql ctype: unknown -> auto"); - } - { - // Emit columns con visibilidad + color rules - State st; - std::vector hdrs = {"lang", "n"}; - std::vector tps = {ColumnType::String, ColumnType::Int}; - st.col_visible = {true, false}; - st.col_order = {1, 0}; - st.color_rules.push_back({0, "go", 0xFF6BB586}); - - std::string out = tql::emit(st, hdrs, tps); - check(out.find("columns") != std::string::npos, "tql emit: include columns"); - check(out.find("visible = false") != std::string::npos, "tql emit: visible=false"); - check(out.find("visible = true") != std::string::npos, "tql emit: visible=true"); - check(out.find("color_rules") != std::string::npos, "tql emit: include color_rules"); - check(out.find("display = \"table\"") != std::string::npos, "tql emit: display table"); - check(out.find("visualization_settings") != std::string::npos, "tql emit: viz settings"); - } - { - // Round-trip columns: emit -> apply -> compare visibility/order/color_rules - State st0; - std::vector hdrs = {"lang", "n"}; - std::vector tps = {ColumnType::String, ColumnType::Int}; - st0.col_visible = {true, false}; - st0.col_order = {1, 0}; - st0.color_rules.push_back({0, "py", 0xFFB5866B}); - - std::string text = tql::emit(st0, hdrs, tps); - - State st1; - const char* cells_t[] = {"go","1","py","2"}; - std::string err; - bool ok = tql::apply(text, st1, hdrs, tps, cells_t, 2, 2, &err); - check(ok, "tql round-trip columns: apply OK"); - check(st1.col_visible.size() == 2 && !st1.col_visible[1], - "tql round-trip: visible[1] = false preservado"); - check(st1.col_order.size() == 2 && st1.col_order[0] == 1 && st1.col_order[1] == 0, - "tql round-trip: col_order [1,0] preservado"); - check(st1.color_rules.size() == 1 && - st1.color_rules[0].col == 0 && - st1.color_rules[0].equals == "py" && - st1.color_rules[0].color == 0xFFB5866B, - "tql round-trip: color_rule preservado"); - } - { - // Apply con expression + columns: type del derived va via columns.type - State st; - std::vector hdrs = {"size_kb"}; - std::vector tps = {ColumnType::Float}; - const char* cells_t[] = {"1.5", "2.0", "3.5"}; - std::string text = R"LUA( -return { - stages = { - { - expressions = { size_bytes = "[size_kb] * 1024" } - } - }, - columns = { - {name = "size_kb", type = "float", visible = true, order = 1}, - {name = "size_bytes", type = "int", visible = true, order = 2, - color_rules = {{equals = "1536", color = "#86b56b"}}}, - } -})LUA"; - std::string err; - bool ok = tql::apply(text, st, hdrs, tps, cells_t, 3, 1, &err); - check(ok, "tql apply: stages + columns combo"); - check(st.raw().derived.size() == 1, "tql apply: derived col size_bytes creada"); - // type override de auto-detect: columns dice "int", aunque auto-detect daria Float - check(st.raw().derived[0].type == ColumnType::Int, - "tql apply: columns.type sobrescribe auto-detect derived"); - // color_rule sobre derived col (idx orig_cols+0 = 1) - check(st.color_rules.size() == 1 && - st.color_rules[0].col == 1 && - st.color_rules[0].equals == "1536", - "tql apply: color_rule sobre derived col"); - // col_order = [size_kb=0, size_bytes=1] - check(st.col_order.size() == 2 && st.col_order[0] == 0 && st.col_order[1] == 1, - "tql apply: col_order desde columns.order"); - } - - // --- lua_string_literal --- - { - check(tql::lua_string_literal("simple") == "\"simple\"", "tql literal: simple"); - check(tql::lua_string_literal("a\"b") == "\"a\\\"b\"", "tql literal: quote escape"); - check(tql::lua_string_literal("a\\b") == "\"a\\\\b\"", "tql literal: backslash escape"); - check(tql::lua_string_literal("a\nb") == "\"a\\nb\"", "tql literal: newline escape"); - } - - // --- Phase 3.1: derived eval sobre stage output --- - { - const char* cells_t[] = { - "go", "core", "10", - "go", "viz", "20", - "py", "core", "30", - "go", "core", "40", - "py", "viz", "50", - "py", "core", "60", - }; - std::vector hdrs = {"lang", "domain", "n"}; - std::vector tps = {ColumnType::String, ColumnType::String, ColumnType::Int}; - Stage s1; - s1.breakouts.push_back("lang"); - s1.aggregations.push_back({AggFn::Count}); - s1.aggregations.push_back({AggFn::Sum, "n"}); - auto out1 = compute_stage(cells_t, 6, 3, hdrs, tps, s1); - - auto* eng = lua_engine::get(); - std::string err; - int id = lua_engine::compile(eng, "[count] * [sum_n]", &err); - check(id >= 0, "phase3.1: compile derived sobre stage output"); - std::vector out_hn = out1.headers; - std::unordered_map n2c; - for (size_t i = 0; i < out_hn.size(); ++i) n2c[out_hn[i]] = (int)i; - std::vector results; - for (int r = 0; r < out1.rows; ++r) { - lua_engine::RowCtx ctx; - ctx.cells = out1.cells.data(); - ctx.orig_cols = out1.cols; - ctx.row = r; - ctx.header_names = &out_hn; - ctx.name_to_col = &n2c; - ctx.types_orig = out1.types.data(); - ctx.n_types_orig = out1.cols; - std::string e; - results.push_back(lua_engine::eval(eng, id, ctx, &e)); - } - int go_idx = -1, py_idx = -1; - for (int r = 0; r < out1.rows; ++r) { - const char* lang = out1.cells[r * out1.cols + 0]; - if (std::strcmp(lang, "go") == 0) go_idx = r; - if (std::strcmp(lang, "py") == 0) py_idx = r; - } - check(go_idx >= 0 && py_idx >= 0, "phase3.1: encontrar grupos go y py"); - check(results[go_idx] == "210", "phase3.1: go count*sum_n = 210"); - check(results[py_idx] == "420", "phase3.1: py count*sum_n = 420"); - lua_engine::release(eng, id); - } - { - // Recursividad: derived B sobre stage output referencia derived A. - const char* cells_t[] = { - "go", "x", - "go", "y", - "py", "z", - }; - std::vector hdrs = {"lang", "name"}; - std::vector tps = {ColumnType::String, ColumnType::String}; - Stage s1; - s1.breakouts.push_back("lang"); - s1.aggregations.push_back({AggFn::Count}); - auto out1 = compute_stage(cells_t, 3, 2, hdrs, tps, s1); - - auto* eng = lua_engine::get(); - std::string err; - int idA = lua_engine::compile(eng, "[count] + 100", &err); - check(idA >= 0, "phase3.1: compile derived A sobre stage output"); - std::vector out_hn = out1.headers; - std::unordered_map n2c; - for (size_t i = 0; i < out_hn.size(); ++i) n2c[out_hn[i]] = (int)i; - std::vector der; - der.push_back({-1, ColumnType::Int, "A", "[count] + 100", idA, ""}); - std::unordered_map dn2i; - dn2i["A"] = 0; - - int idB = lua_engine::compile(eng, "[A] * 2", &err); - check(idB >= 0, "phase3.1: compile derived B refs A"); - - std::vector resB; - for (int r = 0; r < out1.rows; ++r) { - lua_engine::RowCtx ctx; - ctx.cells = out1.cells.data(); - ctx.orig_cols = out1.cols; - ctx.row = r; - ctx.header_names = &out_hn; - ctx.name_to_col = &n2c; - ctx.types_orig = out1.types.data(); - ctx.n_types_orig = out1.cols; - ctx.derived = &der; - ctx.derived_name_to_idx = &dn2i; - std::string e; - resB.push_back(lua_engine::eval(eng, idB, ctx, &e)); - } - int go_idx = -1, py_idx = -1; - for (int r = 0; r < out1.rows; ++r) { - const char* lang = out1.cells[r * out1.cols + 0]; - if (std::strcmp(lang, "go") == 0) go_idx = r; - if (std::strcmp(lang, "py") == 0) py_idx = r; - } - check(resB[go_idx] == "204", "phase3.1: derived B chain (count+100)*2 = 204 go"); - check(resB[py_idx] == "202", "phase3.1: derived B chain (count+100)*2 = 202 py"); - lua_engine::release(eng, idA); - lua_engine::release(eng, idB); - } - - // --- column_type_name + icon no nulos --- - { - const ColumnType all[] = { ColumnType::Auto, ColumnType::String, ColumnType::Int, - ColumnType::Float, ColumnType::Bool, ColumnType::Date, - ColumnType::Json }; - for (auto t : all) { - check(column_type_name(t) != nullptr, "column_type_name no null"); - check(column_type_icon(t) != nullptr, "column_type_icon no null"); - } - } - - // ---------------------------------------------------------------- - // Phase 3: stages vector, multi-stage TQL emit/apply, drill-down. - // ---------------------------------------------------------------- - - // --- State::ensure_stage0 crea stage 0 si vacio --- - { - State st; - check(st.stages.empty(), "phase3 state: stages vacio inicial"); - st.ensure_stage0(); - check(st.stages.size() == 1, "phase3 state: ensure_stage0 crea uno"); - check(st.active_stage == 0, "phase3 state: active_stage default 0"); - } - - // --- raw() y active() devuelven la misma stage cuando active=0 --- - { - State st; - Stage& r = st.raw(); - r.filters.push_back({0, Op::Eq, "x"}); - check(st.active().filters.size() == 1, "phase3 state: active==raw cuando active=0"); - check(st.stages[0].filters.size() == 1, "phase3 state: stages[0] visible via raw()"); - } - - // --- make_drill_filter helper --- - { - Filter f = make_drill_filter(2, "go"); - check(f.col == 2 && f.op == Op::Eq && f.value == "go", - "phase3 drill: make_drill_filter retorna Op::Eq"); - } - - // --- Multi-stage TQL emit: state con stage 0 + stage 1 --- - { - State st; - st.ensure_stage0(); - st.stages[0].filters.push_back({0, Op::Eq, "go"}); - Stage s1; - s1.breakouts.push_back("domain"); - s1.aggregations.push_back({AggFn::Count}); - s1.aggregations.push_back({AggFn::Avg, "n"}); - s1.sorts.push_back({"count", true}); - st.stages.push_back(std::move(s1)); - - std::vector hdrs = {"lang", "domain", "n"}; - std::vector tps = {ColumnType::String, ColumnType::String, ColumnType::Int}; - std::string out = tql::emit(st, hdrs, tps); - - check(out.find("breakout") != std::string::npos, "phase3 emit: contiene breakout"); - check(out.find("\"domain\"") != std::string::npos, "phase3 emit: col domain en breakout"); - check(out.find("aggregation") != std::string::npos, "phase3 emit: contiene aggregation"); - check(out.find("\"count\"") != std::string::npos, "phase3 emit: agg count"); - check(out.find("\"avg\"") != std::string::npos, "phase3 emit: agg avg"); - // 2 stages - size_t first = out.find(" {"); - size_t second = out.find(" {", first + 1); - check(first != std::string::npos && second != std::string::npos, - "phase3 emit: dos stage entries"); - } - - // --- Multi-stage TQL apply: stages chain --- - { - State st; - std::vector hdrs = {"lang", "domain", "n"}; - const char* cells_t[] = { - "go", "core", "10", - "go", "infra", "20", - "py", "core", "30", - "go", "core", "40", - }; - std::string text = R"LUA( -return { - stages = { - { filter = { {"=", "lang", "go"} } }, - { - breakout = {"domain"}, - aggregation = { {"count"}, {"avg", "n"} }, - sort = { {"desc", "count"} }, - }, - } -})LUA"; - std::string err; - bool ok = tql::apply(text, st, hdrs, std::vector{}, cells_t, 4, 3, &err); - check(ok, "phase3 apply: parsea multi-stage"); - check(st.stages.size() == 2, "phase3 apply: 2 stages creados"); - check(st.stages[0].filters.size() == 1 && - st.stages[0].filters[0].col == 0 && - st.stages[0].filters[0].value == "go", - "phase3 apply: stage 0 filter lang=go"); - check(st.stages[1].breakouts.size() == 1 && - st.stages[1].breakouts[0] == "domain", - "phase3 apply: stage 1 breakout=domain"); - check(st.stages[1].aggregations.size() == 2, - "phase3 apply: stage 1 tiene 2 aggregations"); - check(st.stages[1].aggregations[0].fn == AggFn::Count && - st.stages[1].aggregations[1].fn == AggFn::Avg && - st.stages[1].aggregations[1].col == "n", - "phase3 apply: aggregations [count, avg(n)]"); - check(st.stages[1].sorts.size() == 1 && - st.stages[1].sorts[0].col == "count" && - st.stages[1].sorts[0].desc == true, - "phase3 apply: stage 1 sort desc count"); - } - - // --- Chain execution: stage 0 feeds stage 1 (verifica compute_stage cadena) --- - { - std::vector hdrs = {"lang", "domain", "n"}; - std::vector tps = {ColumnType::String, ColumnType::String, ColumnType::Int}; - const char* cells_t[] = { - "go", "core", "10", - "go", "infra", "20", - "py", "core", "30", - "go", "core", "40", - }; - Stage s0; - s0.filters.push_back({0, Op::Eq, "go"}); // lang=go -> 3 rows - auto out0 = compute_stage(cells_t, 4, 3, hdrs, tps, s0); - check(out0.rows == 3, "phase3 chain: stage 0 produce 3 filas"); - - // Stage 1 sobre out0 - Stage s1; - s1.breakouts.push_back("domain"); - s1.aggregations.push_back({AggFn::Count}); - auto out1 = compute_stage(out0.cells.data(), out0.rows, out0.cols, - out0.headers, out0.types, s1); - check(out1.rows == 2, "phase3 chain: stage 1 produce 2 grupos (core,infra)"); - check(out1.cols == 2, "phase3 chain: stage 1 cols = breakout+count"); - check(out1.headers[0] == "domain" && out1.headers[1] == "count", - "phase3 chain: stage 1 headers"); - } - - // --- Round-trip multi-stage emit -> apply -> compare --- - { - State st0; - st0.ensure_stage0(); - Stage s1; - s1.breakouts.push_back("lang"); - s1.aggregations.push_back({AggFn::Count}); - s1.aggregations.push_back({AggFn::Sum, "n"}); - st0.stages.push_back(std::move(s1)); - - std::vector hdrs = {"lang", "n"}; - std::vector tps = {ColumnType::String, ColumnType::Int}; - std::string text = tql::emit(st0, hdrs, tps); - - State st1; - const char* cells_t[] = {"go","10","py","20"}; - std::string err; - bool ok = tql::apply(text, st1, hdrs, tps, cells_t, 2, 2, &err); - check(ok, "phase3 round-trip: apply OK"); - check(st1.stages.size() == 2, "phase3 round-trip: 2 stages preservados"); - check(st1.stages[1].breakouts.size() == 1 && - st1.stages[1].breakouts[0] == "lang", - "phase3 round-trip: breakout preservado"); - check(st1.stages[1].aggregations.size() == 2, - "phase3 round-trip: 2 aggregations preservadas"); - check(st1.stages[1].aggregations[1].fn == AggFn::Sum && - st1.stages[1].aggregations[1].col == "n", - "phase3 round-trip: sum(n) preservado"); - } - - // --- Emit con percentile: incluye arg --- - { - State st; - st.ensure_stage0(); - Stage s1; - s1.breakouts.push_back("k"); - Aggregation pct; pct.fn = AggFn::Percentile; pct.col = "n"; pct.arg = 0.95; - s1.aggregations.push_back(pct); - st.stages.push_back(std::move(s1)); - - std::vector hdrs = {"k", "n"}; - std::vector tps = {ColumnType::String, ColumnType::Int}; - std::string out = tql::emit(st, hdrs, tps); - check(out.find("\"percentile\"") != std::string::npos, - "phase3 emit percentile: fn token"); - check(out.find("0.95") != std::string::npos, - "phase3 emit percentile: arg 0.95"); - } - - // --- Drill-down logica: anadir Filter al stage previo --- - { - // Setup: state con 2 stages. Stage 1 groups by lang. Drill on lang=go - // anade Filter{lang=go} a stage 0 y active=0. - State st; - st.ensure_stage0(); - Stage s1; - s1.breakouts.push_back("lang"); - s1.aggregations.push_back({AggFn::Count}); - st.stages.push_back(std::move(s1)); - st.active_stage = 1; - - // Simular drill: agregar make_drill_filter(0, "go") a stage 0. - st.stages[0].filters.push_back(make_drill_filter(0, "go")); - st.active_stage = 0; - - check(st.stages[0].filters.size() == 1, - "phase3 drill: filter anadido a stage 0"); - check(st.stages[0].filters[0].op == Op::Eq && - st.stages[0].filters[0].value == "go", - "phase3 drill: filter Op::Eq value=go"); - check(st.stages.size() == 2, - "phase3 drill: stage 1 NO se borra (preserva camino)"); - check(st.active_stage == 0, - "phase3 drill: active vuelve a stage 0"); - } - - // === phase5: TQL validacion schema === - { - // version missing -> ok con warning - State st; - std::vector hdrs = {"a"}; - std::string err; - bool ok = tql::apply("return { display=\"table\", stages={}, columns={} }", - st, hdrs, std::vector{}, nullptr, 0, 1, &err); - check(ok, "phase5: version missing acepta"); - check(err.find("version missing") != std::string::npos, - "phase5: warning version missing presente"); - } - { - // version != 1 -> fail - State st; - std::vector hdrs = {"a"}; - std::string err; - bool ok = tql::apply("return { version=999, stages={}, columns={} }", - st, hdrs, std::vector{}, nullptr, 0, 1, &err); - check(!ok, "phase5: version != 1 rechaza"); - check(err.find("unsupported") != std::string::npos, - "phase5: error de version explicito"); - } - { - // version no-numero -> fail - State st; - std::vector hdrs = {"a"}; - std::string err; - bool ok = tql::apply("return { version=\"x\", stages={}, columns={} }", - st, hdrs, std::vector{}, nullptr, 0, 1, &err); - check(!ok, "phase5: version no-number rechaza"); - } - { - // unknown filter col -> warning - State st; - std::vector hdrs = {"a", "b"}; - const char* cells_t[] = {"x","1"}; - std::string err; - std::string text = - "return { version=1, stages={ { filter={{\"=\", \"missing\", \"v\"}} } }, columns={} }"; - bool ok = tql::apply(text, st, hdrs, std::vector{}, - cells_t, 1, 2, &err); - check(ok, "phase5: filter col desconocido NO bloquea"); - check(err.find("filter col") != std::string::npos && err.find("missing") != std::string::npos, - "phase5: warning filter col desconocido"); - } - { - // unknown agg fn -> warning - State st; - std::vector hdrs = {"a", "b"}; - const char* cells_t[] = {"x","1"}; - std::string err; - std::string text = - "return { version=1, stages={ {}, " - "{ breakout={\"a\"}, aggregation={ {\"weirdfn\", \"b\"} } } }, columns={} }"; - bool ok = tql::apply(text, st, hdrs, std::vector{}, - cells_t, 1, 2, &err); - check(ok, "phase5: agg fn desconocida NO bloquea"); - check(err.find("aggregation fn") != std::string::npos, - "phase5: warning agg fn desconocida"); - } - { - // agg sin col cuando la requiere -> warning - State st; - std::vector hdrs = {"a", "b"}; - std::string err; - std::string text = - "return { version=1, stages={ {}, " - "{ breakout={\"a\"}, aggregation={ {\"sum\"} } } }, columns={} }"; - bool ok = tql::apply(text, st, hdrs, std::vector{}, nullptr, 0, 2, &err); - check(ok, "phase5: agg sum sin col NO bloquea"); - check(err.find("requires a column") != std::string::npos, - "phase5: warning agg sin col"); - } - { - // unknown sort dir -> warning - State st; - std::vector hdrs = {"a"}; - std::string err; - std::string text = - "return { version=1, stages={ { sort={ {\"sideways\", \"a\"} } } }, columns={} }"; - bool ok = tql::apply(text, st, hdrs, std::vector{}, nullptr, 0, 1, &err); - check(ok, "phase5: sort dir desconocida NO bloquea"); - check(err.find("sort dir") != std::string::npos, - "phase5: warning sort dir desconocida"); - } - { - // unknown filter op -> warning - State st; - std::vector hdrs = {"a"}; - std::string err; - std::string text = - "return { version=1, stages={ { filter={ {\"~~\", \"a\", \"v\"} } } }, columns={} }"; - bool ok = tql::apply(text, st, hdrs, std::vector{}, nullptr, 0, 1, &err); - check(ok, "phase5: filter op desconocida NO bloquea"); - check(err.find("filter op") != std::string::npos, - "phase5: warning filter op desconocida"); - } - { - // TQL valido -> err vacio - State st; - std::vector hdrs = {"a", "b"}; - const char* cells_t[] = {"x","1","y","2"}; - std::string err; - std::string text = - "return { version=1, stages={ " - "{ filter={ {\"=\",\"a\",\"x\"} }, sort={ {\"asc\",\"a\"} } }, " - "{ breakout={\"a\"}, aggregation={ {\"count\"}, {\"sum\",\"b\"} } } " - "}, columns={} }"; - bool ok = tql::apply(text, st, hdrs, std::vector{}, - cells_t, 2, 2, &err); - check(ok && err.empty(), "phase5: TQL valido sin warnings"); - } - { - // emit() incluye cheatsheet header - State st; - std::vector hdrs = {"a"}; - std::vector tps = {ColumnType::String}; - std::string out = tql::emit(st, hdrs, tps); - check(out.find("-- TQL v1") != std::string::npos, - "phase5: emit incluye comentario cheatsheet"); - check(out.find("-- Stage 0 (Raw)") != std::string::npos, - "phase5: emit incluye explicacion de stages"); - } - - // === phase6: ViewMode tokens + TQL display round-trip === - { - check(std::string(view_mode_token(ViewMode::Table)) == "table", - "phase6: token table"); - check(std::string(view_mode_token(ViewMode::Bar)) == "bar", - "phase6: token bar"); - check(std::string(view_mode_token(ViewMode::Histogram)) == "histogram", - "phase6: token histogram"); - check(view_mode_from_token("scatter") == ViewMode::Scatter, - "phase6: from token scatter"); - check(view_mode_from_token("kpi_grid") == ViewMode::KPIGrid, - "phase6: from token kpi_grid"); - check(view_mode_from_token("nonsense") == ViewMode::Table, - "phase6: token desconocida -> Table default"); - int n; const ViewMode* arr = all_view_modes(&n); - check(arr != nullptr && n >= 20, "phase6: all_view_modes >= 20"); - check(view_mode_min_cols(ViewMode::Bubble) == 3, - "phase6: Bubble requiere 3 cols"); - check(view_mode_needs_category(ViewMode::Pie) == true, - "phase6: Pie necesita category"); - check(view_mode_needs_numeric(ViewMode::Histogram) == true, - "phase6: Histogram necesita numeric"); - } - { - // emit + apply preservan display - State st0; - st0.display = ViewMode::Scatter; - std::vector hdrs = {"a"}; - std::vector tps = {ColumnType::Int}; - std::string text = tql::emit(st0, hdrs, tps); - check(text.find("display = \"scatter\"") != std::string::npos, - "phase6: emit contiene display=scatter"); - - State st1; - const char* cells_t[] = {"1","2","3"}; - std::string err; - bool ok = tql::apply(text, st1, hdrs, tps, cells_t, 3, 1, &err); - check(ok, "phase6: apply ok"); - check(st1.display == ViewMode::Scatter, - "phase6: display preservado tras round-trip"); - } - { - // display desconocido -> Table default + warning - State st; - std::vector hdrs = {"a"}; - std::string err; - std::string text = - "return { version=1, display=\"weird\", stages={}, columns={} }"; - bool ok = tql::apply(text, st, hdrs, std::vector{}, nullptr, 0, 1, &err); - check(ok, "phase6: display unknown NO bloquea"); - check(st.display == ViewMode::Table, - "phase6: display unknown -> Table default"); - check(err.find("unknown display") != std::string::npos, - "phase6: warning unknown display"); - } - - // === phase7b: TQL views round-trip === - { - State st0; - st0.display = ViewMode::Bar; - st0.viz_config.cat_col = "country"; - st0.viz_config.y_cols = {"sales"}; - st0.viz_config.primary_color = 0xFF00FF00; - - VizPanel p; - p.display = ViewMode::Pie; - p.config.cat_col = "country"; - p.config.y_cols = {"profit"}; - p.config.hist_bins = 0; - p.config.show_legend = false; - st0.extra_panels.push_back(p); - - VizPanel p2; - p2.display = ViewMode::Histogram; - p2.config.y_cols = {"sales"}; - p2.config.hist_bins = 20; - st0.extra_panels.push_back(p2); - - std::vector hdrs = {"country", "sales", "profit"}; - std::vector tps = {ColumnType::String, ColumnType::Int, ColumnType::Int}; - std::string text = tql::emit(st0, hdrs, tps); - check(text.find("views = {") != std::string::npos, - "phase7b: emit contiene bloque views"); - check(text.find("display = \"bar\"") != std::string::npos, - "phase7b: emit panel 0 display=bar"); - check(text.find("display = \"pie\"") != std::string::npos, - "phase7b: emit panel 1 display=pie"); - check(text.find("display = \"histogram\"") != std::string::npos, - "phase7b: emit panel 2 display=histogram"); - - State st1; - const char* cells_t[] = {"es","100","20","fr","200","30"}; - std::string err; - bool ok = tql::apply(text, st1, hdrs, tps, cells_t, 2, 3, &err); - check(ok, "phase7b: apply views ok"); - check(st1.display == ViewMode::Bar, "phase7b: main display preservado"); - check(st1.viz_config.cat_col == "country", - "phase7b: main cat_col preservado"); - check(st1.extra_panels.size() == 2, - "phase7b: 2 extra panels preservados"); - if (st1.extra_panels.size() >= 2) { - check(st1.extra_panels[0].display == ViewMode::Pie, - "phase7b: extra[0] = pie"); - check(st1.extra_panels[1].display == ViewMode::Histogram, - "phase7b: extra[1] = histogram"); - check(st1.extra_panels[1].config.hist_bins == 20, - "phase7b: hist_bins preservado"); - check(st1.extra_panels[0].config.show_legend == false, - "phase7b: show_legend=false preservado"); - } - } - - // === phase9: joins MBQL-style === - { - // Left table: users - std::vector lh = {"id", "name"}; - std::vector lt = {ColumnType::Int, ColumnType::String}; - const char* lc[] = {"1","alice", "2","bob", "3","carol"}; - - // Right table: orders - TableInput right; - right.name = "orders"; - right.headers = {"user_id", "amount"}; - right.types = {ColumnType::Int, ColumnType::Int}; - const char* rc[] = {"1","100", "1","200", "2","50", "4","999"}; - right.cells = rc; - right.rows = 4; - right.cols = 2; - - Join jn; - jn.alias = "o"; - jn.source = "orders"; - jn.on = {{"id", "user_id"}}; - jn.strategy = JoinStrategy::Inner; - - auto out = join_tables(lc, 3, 2, lh, lt, right, jn); - check(out.cols == 4, "phase9 inner: 4 cols"); - check(out.rows == 3, "phase9 inner: 3 matches (1+1+2 minus carol)"); - check(out.headers[2] == "o.user_id", - "phase9 inner: header prefijado alias.col"); - check(out.headers[3] == "o.amount", - "phase9 inner: header amount prefijado"); - - // Left join: alice/alice/bob/carol(empty) - jn.strategy = JoinStrategy::Left; - auto out_l = join_tables(lc, 3, 2, lh, lt, right, jn); - check(out_l.rows == 4, "phase9 left: 4 filas (3 matches + carol empty)"); - - // Right join: alice/alice/bob/empty(user 4) - jn.strategy = JoinStrategy::Right; - auto out_r = join_tables(lc, 3, 2, lh, lt, right, jn); - check(out_r.rows == 4, "phase9 right: 4 filas (3 matches + user 4 empty)"); - - // Full join: 3 matches + carol-empty + user4-empty = 5 - jn.strategy = JoinStrategy::Full; - auto out_f = join_tables(lc, 3, 2, lh, lt, right, jn); - check(out_f.rows == 5, "phase9 full: 5 filas"); - - // Sin alias -> headers del right sin prefijo (preservar nombre) - jn.alias = ""; - jn.strategy = JoinStrategy::Inner; - auto out_nopfx = join_tables(lc, 3, 2, lh, lt, right, jn); - check(out_nopfx.headers[2] == "user_id", - "phase9: sin alias headers no prefijados"); - - // Fields filter: solo "amount" - jn.alias = "o"; - jn.fields = {"amount"}; - auto out_ff = join_tables(lc, 3, 2, lh, lt, right, jn); - check(out_ff.cols == 3, "phase9: fields filter -> solo 1 col del right"); - check(out_ff.headers[2] == "o.amount", - "phase9: fields filter respeta alias"); - } - { - // Multi-key join - std::vector lh = {"y", "m", "v"}; - std::vector lt = {ColumnType::Int, ColumnType::Int, ColumnType::Int}; - const char* lc[] = {"2020","1","10", "2020","2","20", "2021","1","30"}; - - TableInput right; - right.name = "tax"; - right.headers = {"year", "month", "rate"}; - right.types = {ColumnType::Int, ColumnType::Int, ColumnType::Float}; - const char* rc[] = {"2020","1","0.1", "2020","2","0.15", "2021","1","0.2"}; - right.cells = rc; right.rows = 3; right.cols = 3; - - Join jn; - jn.alias = "t"; - jn.source = "tax"; - jn.on = {{"y","year"}, {"m","month"}}; - jn.strategy = JoinStrategy::Inner; - - auto out = join_tables(lc, 3, 3, lh, lt, right, jn); - check(out.rows == 3, "phase9 multi-key: 3 matches"); - check(out.cols == 6, "phase9 multi-key: 3 left + 3 right"); - } - { - // TQL main_source round-trip - State st0; - st0.main_source = "users"; - std::vector hdrs = {"a"}; - std::vector tps = {ColumnType::String}; - std::string text = tql::emit(st0, hdrs, tps); - check(text.find("main_source = \"users\"") != std::string::npos, - "phase9 TQL: emit main_source"); - State st1; - std::string err; - bool ok = tql::apply(text, st1, hdrs, tps, nullptr, 0, 1, &err); - check(ok, "phase9 TQL: apply main_source ok"); - check(st1.main_source == "users", "phase9 TQL: main_source preservado"); - } - { - // TQL emit/apply joins - State st0; - Join jn; - jn.alias = "o"; jn.source = "orders"; jn.strategy = JoinStrategy::Inner; - jn.on = {{"user_id", "user_id"}, {"region", "region"}}; - jn.fields = {"amount", "tax"}; - st0.joins.push_back(jn); - - std::vector hdrs = {"user_id","region","name"}; - std::vector tps = {ColumnType::Int, ColumnType::String, ColumnType::String}; - std::string text = tql::emit(st0, hdrs, tps); - check(text.find("joins = {") != std::string::npos, "phase9 TQL: emit joins block"); - check(text.find("strategy = \"inner\"") != std::string::npos, "phase9 TQL: emit strategy"); - check(text.find("fields = {") != std::string::npos, "phase9 TQL: emit fields"); - - State st1; - std::string err; - bool ok = tql::apply(text, st1, hdrs, tps, nullptr, 0, 3, &err); - check(ok, "phase9 TQL: apply ok"); - check(st1.joins.size() == 1, "phase9 TQL: 1 join preservado"); - if (!st1.joins.empty()) { - check(st1.joins[0].alias == "o", "phase9 TQL: alias preservado"); - check(st1.joins[0].strategy == JoinStrategy::Inner, "phase9 TQL: strategy preservada"); - check(st1.joins[0].on.size() == 2, "phase9 TQL: multi-key on preservada"); - check(st1.joins[0].fields.size() == 2, "phase9 TQL: fields preservados"); - } - } - { - // resolve_main_idx - std::vector empty; - check(resolve_main_idx(empty, "") == -1, "phase9: tables vacio -> -1"); - TableInput a; a.name = "a"; - TableInput b; b.name = "b"; - TableInput c; c.name = "c"; - std::vector t3 = {a, b, c}; - check(resolve_main_idx(t3, "") == 0, "phase9: source vacio -> idx 0"); - check(resolve_main_idx(t3, "b") == 1, "phase9: source match -> idx exacto"); - check(resolve_main_idx(t3, "c") == 2, "phase9: source match c -> 2"); - check(resolve_main_idx(t3, "nope") == 0, "phase9: source desconocido -> idx 0"); - } - { - // Strategy tokens round-trip - check(std::string(join_strategy_token(JoinStrategy::Left)) == "left", "phase9: token left"); - check(std::string(join_strategy_token(JoinStrategy::Inner)) == "inner","phase9: token inner"); - check(join_strategy_from_token("right") == JoinStrategy::Right, "phase9: parse right"); - check(join_strategy_from_token("full") == JoinStrategy::Full, "phase9: parse full"); - check(join_strategy_from_token("nope") == JoinStrategy::Left, "phase9: parse fallback left"); - } - - // === phase10: drill extendido === - { - // truncate_date — granularities sobre 2026-05-12 (martes). - std::string d = "2026-05-12"; - check(truncate_date(d, DateGranularity::Year) == "2026", "phase10: trunc year"); - check(truncate_date(d, DateGranularity::Month) == "2026-05", "phase10: trunc month"); - check(truncate_date(d, DateGranularity::Day) == "2026-05-12", "phase10: trunc day"); - check(truncate_date(d, DateGranularity::Week) == "2026-05-11", "phase10: trunc week (Mon)"); - check(truncate_date("2026-05-12T14:33:01", DateGranularity::Hour) == "2026-05-12T14", - "phase10: trunc hour"); - check(truncate_date("not-a-date", DateGranularity::Month) == "not-a-date", - "phase10: trunc passthrough invalido"); - check(truncate_date(d, DateGranularity::None) == d, "phase10: trunc None == identidad"); - } - - { - // auto_date_granularity - check(auto_date_granularity("2024-01-01", "2026-05-12") == DateGranularity::Year, - "phase10: auto year >2y"); - check(auto_date_granularity("2026-01-01", "2026-05-12") == DateGranularity::Month, - "phase10: auto month >60d"); - check(auto_date_granularity("2026-04-15", "2026-05-12") == DateGranularity::Week, - "phase10: auto week >14d"); - check(auto_date_granularity("2026-05-05", "2026-05-12") == DateGranularity::Day, - "phase10: auto day <=14d"); - check(auto_date_granularity("bad", "2026-05-12") == DateGranularity::Day, - "phase10: auto fallback day"); - } - - { - // parse_breakout_granularity - std::string col; - check(parse_breakout_granularity("ts:month", col) == DateGranularity::Month, - "phase10: parse breakout month"); - check(col == "ts", "phase10: parse breakout col stripped"); - check(parse_breakout_granularity("ts", col) == DateGranularity::None, - "phase10: parse breakout sin sufijo None"); - check(col == "ts", "phase10: col sin sufijo intacto"); - check(parse_breakout_granularity("ts:wat", col) == DateGranularity::None, - "phase10: sufijo desconocido None"); - check(col == "ts:wat", "phase10: col preserva sufijo desconocido"); - } - - { - // compose_breakout - check(compose_breakout("ts", DateGranularity::None) == "ts", "phase10: compose None"); - check(compose_breakout("ts", DateGranularity::Month) == "ts:month", "phase10: compose month"); - check(compose_breakout("ts", DateGranularity::Year) == "ts:year", "phase10: compose year"); - // round-trip parse(compose) - std::string col; - auto g = parse_breakout_granularity(compose_breakout("foo", DateGranularity::Week), col); - check(g == DateGranularity::Week && col == "foo", "phase10: compose+parse round-trip"); - } - - { - // column_min_max - const char* cells[] = { - "2026-03-01", - "2026-01-15", - "", - "2026-05-12", - "2026-02-22", - }; - std::string lo, hi; - column_min_max(cells, 5, 1, 0, lo, hi); - check(lo == "2026-01-15" && hi == "2026-05-12", "phase10: column_min_max ISO ordena lexical"); - - const char* empty_cells[] = {"", "", ""}; - column_min_max(empty_cells, 3, 1, 0, lo, hi); - check(lo.empty() && hi.empty(), "phase10: column_min_max sin datos -> vacio"); - - column_min_max(cells, 5, 1, 5, lo, hi); // col fuera de rango - check(lo.empty() && hi.empty(), "phase10: column_min_max col fuera de rango -> vacio"); - } - - { - // tokens round-trip granularity - check(date_granularity_from_token("year") == DateGranularity::Year, "phase10: token year"); - check(date_granularity_from_token("month") == DateGranularity::Month, "phase10: token month"); - check(date_granularity_from_token("week") == DateGranularity::Week, "phase10: token week"); - check(date_granularity_from_token("day") == DateGranularity::Day, "phase10: token day"); - check(date_granularity_from_token("hour") == DateGranularity::Hour, "phase10: token hour"); - check(date_granularity_from_token("nope") == DateGranularity::None, "phase10: token fallback None"); - check(std::string(date_granularity_token(DateGranularity::Month)) == "month", - "phase10: emit month"); - check(std::string(date_granularity_token(DateGranularity::None)) == "", - "phase10: emit None empty"); - } - - { - // build_preset_filters - auto f7 = build_preset_filters(FilterPreset::Last7d, 2, "2026-05-12"); - check(f7.size() == 1, "phase10: Last7d -> 1 filter"); - check(f7[0].col == 2 && f7[0].op == Op::Gte && f7[0].value == "2026-05-05", - "phase10: Last7d -> Gte 2026-05-05"); - - auto f30 = build_preset_filters(FilterPreset::Last30d, 2, "2026-05-12"); - check(f30[0].value == "2026-04-12", "phase10: Last30d -> 2026-04-12"); - - auto f90 = build_preset_filters(FilterPreset::Last90d, 2, "2026-05-12"); - check(f90[0].value == "2026-02-11", "phase10: Last90d -> 2026-02-11"); - - auto fn0 = build_preset_filters(FilterPreset::ExcludeNulls, 3, ""); - check(fn0.size() == 1 && fn0[0].op == Op::Neq && fn0[0].value == "", - "phase10: ExcludeNulls -> Neq ''"); - - auto fnz = build_preset_filters(FilterPreset::NonZero, 4, ""); - check(fnz.size() == 2, "phase10: NonZero -> 2 filters"); - check(fnz[0].op == Op::Neq && fnz[0].value == "" && - fnz[1].op == Op::Neq && fnz[1].value == "0", - "phase10: NonZero -> Neq '' AND Neq '0'"); - - auto fbad = build_preset_filters(FilterPreset::Last7d, 2, "bad-date"); - check(fbad.empty(), "phase10: Last7d con today invalido -> empty"); - } - - { - // TQL round-trip: breakout con sufijo :granularity. - State st0; - st0.stages.resize(2); - st0.stages[1].breakouts = {"ts:month"}; - Aggregation a; a.fn = AggFn::Count; a.alias = "n"; - st0.stages[1].aggregations.push_back(a); - - std::vector hdrs = {"ts", "amount"}; - std::vector tys = {ColumnType::Date, ColumnType::Float}; - int eff = 2; - std::string text = tql::emit(st0, hdrs, tys); - check(text.find("\"ts:month\"") != std::string::npos, - "phase10 TQL: emit breakout granularity sufijo"); - - std::string err; - State st1; - bool ok = tql::apply(text, st1, hdrs, tys, nullptr, 2, eff, &err); - check(ok, "phase10 TQL: apply round-trip ok"); - check(st1.stages.size() >= 2 && st1.stages[1].breakouts.size() == 1 && - st1.stages[1].breakouts[0] == "ts:month", - "phase10 TQL: breakout granularity preservada"); - } - - { - // compute_stage aplica truncado de fecha cuando hay :granularity. - const char* cells[] = { - "2026-01-15", "10", - "2026-01-22", "20", - "2026-02-03", "30", - "2026-03-11", "40", - }; - std::vector hdrs = {"ts", "amount"}; - std::vector tys = {ColumnType::Date, ColumnType::Float}; - Stage s1; - s1.breakouts = {"ts:month"}; - Aggregation ag; ag.fn = AggFn::Count; ag.alias = "n"; - s1.aggregations.push_back(ag); - auto out = compute_stage(cells, 4, 2, hdrs, tys, s1); - check(out.rows == 3, "phase10: trunc month -> 3 grupos (Jan/Feb/Mar)"); - check(out.headers[0] == "ts:month", "phase10: header preserva sufijo"); - // Verifica que algun valor de breakout es "2026-01" - bool found_jan = false; - for (int r = 0; r < out.rows; ++r) { - if (std::string(out.cells[r * out.cols + 0]) == "2026-01") found_jan = true; - } - check(found_jan, "phase10: trunc value '2026-01' presente"); - } - - // === phase10 hit-tests para click-to-drill === - { - // nearest_index_1d - double xs[] = {0, 1, 2, 3, 4}; - check(nearest_index_1d(0.0, xs, 5) == 0, "phase10 hit: nearest_1d exact 0"); - check(nearest_index_1d(2.4, xs, 5) == 2, "phase10 hit: nearest_1d 2.4 -> 2"); - check(nearest_index_1d(2.6, xs, 5) == 3, "phase10 hit: nearest_1d 2.6 -> 3"); - check(nearest_index_1d(-1.0, xs, 5) == 0, "phase10 hit: nearest_1d clamp left"); - check(nearest_index_1d(99.0, xs, 5) == 4, "phase10 hit: nearest_1d clamp right"); - check(nearest_index_1d(0.0, nullptr, 0) == -1, "phase10 hit: nearest_1d empty -> -1"); - } - - { - // nearest_index_2d - double xs[] = {0, 10, 5, 5}; - double ys[] = {0, 0, 10, 5}; - check(nearest_index_2d(0.1, 0.1, xs, ys, 4) == 0, "phase10 hit: nearest_2d cerca de (0,0)"); - check(nearest_index_2d(9.9, 0.0, xs, ys, 4) == 1, "phase10 hit: nearest_2d cerca de (10,0)"); - check(nearest_index_2d(5.0, 4.9, xs, ys, 4) == 3, "phase10 hit: nearest_2d cerca de (5,5)"); - check(nearest_index_2d(0, 0, nullptr, nullptr, 0) == -1, "phase10 hit: nearest_2d empty -> -1"); - } - - { - // pie_angle (convencion ImPlot: 0 = top, sentido horario) - const double PI = 3.14159265358979323846; - double a; - a = pie_angle(0.5, 0.5, 0.5, 0.0); // top - check(std::fabs(a - 0.0) < 1e-9, "phase10 hit: pie_angle top = 0"); - a = pie_angle(0.5, 0.5, 1.0, 0.5); // right -> PI/2 - check(std::fabs(a - PI/2) < 1e-9, "phase10 hit: pie_angle right = PI/2"); - a = pie_angle(0.5, 0.5, 0.5, 1.0); // bottom -> PI - check(std::fabs(a - PI) < 1e-9, "phase10 hit: pie_angle bottom = PI"); - a = pie_angle(0.5, 0.5, 0.0, 0.5); // left -> 3*PI/2 - check(std::fabs(a - 3*PI/2) < 1e-9, "phase10 hit: pie_angle left = 3PI/2"); - } - - { - // pie_slice_at_angle: 4 slices iguales -> cada uno cubre PI/2. - double sums[] = {1.0, 1.0, 1.0, 1.0}; - const double PI = 3.14159265358979323846; - check(pie_slice_at_angle(0.0, sums, 4) == 0, "phase10 hit: slice 0 (top)"); - check(pie_slice_at_angle(PI/4, sums, 4) == 0, "phase10 hit: slice 0 (mid)"); - check(pie_slice_at_angle(PI/2 + 0.1, sums, 4) == 1, "phase10 hit: slice 1"); - check(pie_slice_at_angle(PI + 0.1, sums, 4) == 2, "phase10 hit: slice 2"); - check(pie_slice_at_angle(3*PI/2 + 0.1, sums, 4) == 3, "phase10 hit: slice 3"); - - double zeros[] = {0.0, 0.0}; - check(pie_slice_at_angle(0.5, zeros, 2) == -1, "phase10 hit: total 0 -> -1"); - check(pie_slice_at_angle(0.0, nullptr, 0) == -1, "phase10 hit: empty -> -1"); - - double neg[] = {1.0, -1.0}; - check(pie_slice_at_angle(0.5, neg, 2) == -1, "phase10 hit: neg sum -> -1"); - } - - { - // heatmap_cell_at - int rr, cc; - heatmap_cell_at(1.5, 2.5, 4, 3, rr, cc); - check(rr == 2 && cc == 1, "phase10 hit: heatmap (1.5,2.5) en 4x3 -> r2 c1"); - heatmap_cell_at(-1, 0, 4, 3, rr, cc); - check(rr == -1 && cc == -1, "phase10 hit: heatmap fuera de rango"); - heatmap_cell_at(0, 0, 0, 0, rr, cc); - check(rr == -1 && cc == -1, "phase10 hit: heatmap empty"); - } - - { - // E2E click-to-drill: simular pipeline stage1 agrupado, click en row idx 2. - State st; - st.stages.resize(2); - std::vector hdrs = {"lang", "n"}; - std::vector tys = {ColumnType::String, ColumnType::Int}; - st.stages[1].breakouts.push_back("lang"); - st.stages[1].aggregations.push_back({AggFn::Count}); - st.active_stage = 1; - - // Stage 1 output simulado (3 grupos). - const char* g_cells[] = { - "go", "3", - "py", "2", - "cpp", "1", - }; - StageOutput so; - so.cells.insert(so.cells.end(), g_cells, g_cells + 6); - so.rows = 3; - so.cols = 2; - so.headers = {"lang", "count"}; - - // Simular click en row idx 2 (cpp). - int clicked_row = 2; - int n_brk = (int)st.stages[1].breakouts.size(); - check(n_brk == 1, "phase10 e2e: 1 breakout"); - const char* v = so.cells[clicked_row * so.cols + 0]; - std::string col_clean; - parse_breakout_granularity(so.headers[0], col_clean); - check(col_clean == "lang", "phase10 e2e: col_clean stripped OK"); - st.stages[0].filters.push_back(make_drill_filter(0, v)); - st.active_stage = 0; - - check(st.active_stage == 0, "phase10 e2e: active retrocede a 0"); - check(st.stages[0].filters.size() == 1, "phase10 e2e: 1 filter anadido"); - check(st.stages[0].filters[0].col == 0 && - st.stages[0].filters[0].op == Op::Eq && - st.stages[0].filters[0].value == "cpp", - "phase10 e2e: filter Op::Eq col=0 value=cpp"); - } - - // === phase10 drill history (apply/undo step) === - { - State st; - st.stages.resize(2); - st.active_stage = 1; - - DrillStep step; - step.target_stage = 0; - step.filter_pos = 0; - step.prev_active_stage = 1; - step.added = make_drill_filter(0, "go"); - - check(apply_drill_step(st, step), "phase10 hist: apply ok"); - check(st.stages[0].filters.size() == 1, "phase10 hist: filter anadido"); - check(st.stages[0].filters[0].value == "go", "phase10 hist: value preservado"); - check(st.active_stage == 0, "phase10 hist: active = target"); - - check(undo_drill_step(st, step), "phase10 hist: undo ok"); - check(st.stages[0].filters.empty(), "phase10 hist: filter eliminado"); - check(st.active_stage == 1, "phase10 hist: active restaurado"); - - // Redo - check(apply_drill_step(st, step), "phase10 hist: redo ok"); - check(st.stages[0].filters.size() == 1, "phase10 hist: redo filter de vuelta"); - check(st.active_stage == 0, "phase10 hist: redo active retorna"); - - // Edge: target fuera de rango - DrillStep bad; - bad.target_stage = 99; - check(!apply_drill_step(st, bad), "phase10 hist: apply fuera de rango -> false"); - check(!undo_drill_step(st, bad), "phase10 hist: undo fuera de rango -> false"); - - // Edge: pos invalida - DrillStep bad_pos = step; - bad_pos.filter_pos = 99; - check(!undo_drill_step(st, bad_pos), "phase10 hist: undo pos invalida -> false"); - } - - // === phase10 drill history: back/forward stack semantics simulado === - { - State st; - st.stages.resize(3); - st.active_stage = 2; - - std::vector back_stack; - std::vector fwd_stack; - - auto drill = [&](int from, int target, int pos, int col, const std::string& v) { - DrillStep s; - s.target_stage = target; - s.filter_pos = pos; - s.prev_active_stage = from; - s.added = make_drill_filter(col, v); - apply_drill_step(st, s); - back_stack.push_back(s); - fwd_stack.clear(); - }; - - drill(2, 1, 0, 0, "go"); - check(st.stages[1].filters.size() == 1, "phase10 hist seq: drill1 aplicado"); - drill(1, 0, 0, 1, "10"); - check(st.stages[0].filters.size() == 1, "phase10 hist seq: drill2 aplicado"); - check(back_stack.size() == 2, "phase10 hist seq: back stack 2"); - check(fwd_stack.empty(), "phase10 hist seq: forward limpio"); - - // Back x1 - DrillStep s = back_stack.back(); back_stack.pop_back(); - undo_drill_step(st, s); - fwd_stack.push_back(s); - check(st.stages[0].filters.empty(), "phase10 hist seq: back deshace drill2"); - check(st.active_stage == 1, "phase10 hist seq: back restaura active=1"); - check(fwd_stack.size() == 1, "phase10 hist seq: fwd stack 1"); - - // Forward x1 - s = fwd_stack.back(); fwd_stack.pop_back(); - apply_drill_step(st, s); - back_stack.push_back(s); - check(st.stages[0].filters.size() == 1, "phase10 hist seq: forward reaplica"); - check(st.active_stage == 0, "phase10 hist seq: forward active=0"); - } - - // === phase10 row inspector (row_to_tsv + build_filters_from_row) === - { - const char* cells[] = { - "go", "10", "filter", - "py", "20", "sma", - "go", "30", "map", - }; - std::vector hdrs = {"lang", "n", "fn"}; - - std::string tsv = row_to_tsv(cells, 3, 3, 1, hdrs); - check(tsv == "lang\tn\tfn\r\npy\t20\tsma\r\n", - "phase10 inspect: row_to_tsv layout"); - - check(row_to_tsv(cells, 3, 3, -1, hdrs).empty(), "phase10 inspect: tsv neg row -> empty"); - check(row_to_tsv(cells, 3, 3, 5, hdrs).empty(), "phase10 inspect: tsv row oob -> empty"); - check(row_to_tsv(cells, 3, 0, 0, hdrs).empty(), "phase10 inspect: tsv cols=0 -> empty"); - - auto fs = build_filters_from_row(cells, 3, 3, 0); - check(fs.size() == 3, "phase10 inspect: 3 filters de row 0"); - check(fs[0].col == 0 && fs[0].op == Op::Eq && fs[0].value == "go", - "phase10 inspect: filter[0] col=0 op=Eq value=go"); - check(fs[2].value == "filter", "phase10 inspect: filter[2] value=filter"); - - // Row con celda vacia -> filter saltado - const char* sparse[] = {"a", "", "c"}; - auto fs2 = build_filters_from_row(sparse, 1, 3, 0); - check(fs2.size() == 2 && fs2[0].col == 0 && fs2[1].col == 2, - "phase10 inspect: cells vacios salteados"); - - check(build_filters_from_row(cells, 3, 3, -1).empty(), - "phase10 inspect: build_filters row invalido -> empty"); - } - - // === phase10 drill-up === - { - State st; - st.stages.resize(3); - st.active_stage = 2; - check(drill_up(st), "phase10 up: 2->1 ok"); - check(st.active_stage == 1, "phase10 up: active=1"); - check(drill_up(st), "phase10 up: 1->0 ok"); - check(st.active_stage == 0, "phase10 up: active=0"); - check(!drill_up(st), "phase10 up: 0 -> false"); - check(st.active_stage == 0, "phase10 up: queda en 0"); - - // Filters no se mueven - State st2; - st2.stages.resize(2); - st2.active_stage = 1; - st2.stages[1].filters.push_back({0, Op::Eq, "x"}); - drill_up(st2); - check(st2.stages[0].filters.empty() && st2.stages[1].filters.size() == 1, - "phase10 up: filters quedan en su stage"); - - State empty_st; - check(!drill_up(empty_st), "phase10 up: stages vacio -> false"); - } - - // === phase11: Lua subset validator + transpiler === - { - std::string err; - // Subset OK: literales + ops - std::string e1 = tql_to_sql::transpile_expr("1 + 2", {}, err); - check(err.empty() && e1.find("1 + 2") != std::string::npos, - "phase11 lua: literal arith"); - - std::string e2 = tql_to_sql::transpile_expr("[a] + [b] * 2", {}, err); - check(err.empty() && e2.find("\"a\"") != std::string::npos && - e2.find("\"b\"") != std::string::npos, - "phase11 lua: col refs + arith"); - - std::string e3 = tql_to_sql::transpile_expr("[a] .. \"_\" .. [b]", {}, err); - check(err.empty() && e3.find(" || ") != std::string::npos, - "phase11 lua: concat -> ||"); - - std::string e4 = tql_to_sql::transpile_expr( - "if [n] > 10 then \"big\" else \"small\" end", {}, err); - check(err.empty() && e4.find("CASE WHEN") != std::string::npos && - e4.find("THEN") != std::string::npos && e4.find("ELSE") != std::string::npos, - "phase11 lua: if/then/else -> CASE"); - - std::string e5 = tql_to_sql::transpile_expr("math.floor([x] / 100)", {}, err); - check(err.empty() && e5.find("floor(") != std::string::npos, - "phase11 lua: math.floor"); - - std::string e6 = tql_to_sql::transpile_expr("string.upper([name])", {}, err); - check(err.empty() && e6.find("upper(") != std::string::npos, - "phase11 lua: string.upper"); - - std::string e7 = tql_to_sql::transpile_expr("string.sub([s], 1, 3)", {}, err); - check(err.empty() && e7.find("substring(") != std::string::npos, - "phase11 lua: string.sub 3-arg"); - - std::string e8 = tql_to_sql::transpile_expr("not ([x] == nil)", {}, err); - check(err.empty() && e8.find("NOT") != std::string::npos && e8.find("NULL") != std::string::npos, - "phase11 lua: not + nil"); - - std::string e9 = tql_to_sql::transpile_expr("tonumber([n])", {}, err); - check(err.empty() && e9.find("CAST(") != std::string::npos, - "phase11 lua: tonumber -> CAST DOUBLE"); - - // Fuera subset: 9 categorias rechazadas - err.clear(); - check(tql_to_sql::transpile_expr("function() return 1 end", {}, err).empty() - && err.find("closures") != std::string::npos, - "phase11 lua: function closure rechazado"); - - err.clear(); - check(tql_to_sql::transpile_expr("local x = 1", {}, err).empty() - && err.find("local") != std::string::npos, - "phase11 lua: local rechazado"); - - err.clear(); - check(tql_to_sql::transpile_expr("for i=1,10 do end", {}, err).empty() - && err.find("loops") != std::string::npos, - "phase11 lua: for loop rechazado"); - - err.clear(); - check(tql_to_sql::transpile_expr("while true do end", {}, err).empty() - && err.find("loops") != std::string::npos, - "phase11 lua: while loop rechazado"); - - err.clear(); - check(tql_to_sql::transpile_expr("{1,2,3}", {}, err).empty() - && err.find("table") != std::string::npos, - "phase11 lua: table literal rechazado"); - - err.clear(); - check(tql_to_sql::transpile_expr("io.read()", {}, err).empty() - && err.find("io") != std::string::npos, - "phase11 lua: io.* rechazado"); - - err.clear(); - check(tql_to_sql::transpile_expr("string.gsub([s], \"a\", \"b\")", {}, err).empty() - && err.find("whitelist") != std::string::npos, - "phase11 lua: string.gsub no whitelisted"); - - err.clear(); - check(tql_to_sql::transpile_expr("print([x])", {}, err).empty() - && err.find("print") != std::string::npos, - "phase11 lua: print rechazado"); - - err.clear(); - check(tql_to_sql::transpile_expr("[a]; [b]", {}, err).empty() - && err.find("multi-statement") != std::string::npos, - "phase11 lua: ';' multi-stmt rechazado"); - - // is_transpilable wrapper - std::string werr; - check(tql_to_sql::is_transpilable("[a] + 1", werr), "phase11 lua: is_transpilable OK"); - check(!tql_to_sql::is_transpilable("function() end", werr), - "phase11 lua: is_transpilable false para closure"); - } - - // === phase11: TQL State -> SQL DuckDB emit === - { - // Setup: 1 tabla "users" con cols lang,n. - TableInput t; - t.name = "users"; - t.headers = {"lang", "n"}; - t.types = {ColumnType::String, ColumnType::Int}; - // Cells no usado por emit (solo schema). - std::vector tables = {t}; - - // Caso 1: stage 0 simple (sin filters ni sort) - { - State st; - st.stages.resize(1); - auto e = tql_to_sql::emit_sql(st, tables); - check(e.error.empty(), "phase11 sql: empty pipeline -> no error"); - check(e.sql.find("WITH t0") != std::string::npos && - e.sql.find("FROM \"users\"") != std::string::npos && - e.sql.find("SELECT * FROM t0") != std::string::npos, - "phase11 sql: stage0 SELECT * FROM users"); - } - - // Caso 2: stage 0 filter + sort - { - State st; - st.stages.resize(1); - st.stages[0].filters.push_back({0, Op::Eq, "go"}); - st.stages[0].filters.push_back({1, Op::Gt, "10"}); - st.stages[0].sorts.push_back({"n", true}); - auto e = tql_to_sql::emit_sql(st, tables); - check(e.error.empty(), "phase11 sql: filter+sort OK"); - check(e.sql.find("WHERE") != std::string::npos && - e.sql.find("\"lang\" = ?") != std::string::npos && - e.sql.find("\"n\" > ?") != std::string::npos, - "phase11 sql: filter clauses"); - check(e.params.size() == 2 && e.params[0] == "go" && e.params[1] == "10", - "phase11 sql: params bound"); - check(e.sql.find("ORDER BY \"n\" DESC") != std::string::npos, - "phase11 sql: ORDER BY desc"); - } - - // Caso 3: stage 1 group + count - { - State st; - st.stages.resize(2); - st.stages[1].breakouts.push_back("lang"); - st.stages[1].aggregations.push_back({AggFn::Count}); - st.active_stage = 1; - auto e = tql_to_sql::emit_sql(st, tables); - check(e.error.empty(), "phase11 sql: group ok"); - check(e.sql.find("t1 AS") != std::string::npos && - e.sql.find("COUNT(*)") != std::string::npos && - e.sql.find("GROUP BY") != std::string::npos && - e.sql.find("SELECT * FROM t1") != std::string::npos, - "phase11 sql: stage1 CTE + COUNT + GROUP BY"); - } - - // Caso 4: granularity :month -> date_trunc - { - State st; - st.stages.resize(2); - st.stages[1].breakouts.push_back("ts:month"); - st.stages[1].aggregations.push_back({AggFn::Sum, "n"}); - st.active_stage = 1; - TableInput ts_t; - ts_t.name = "events"; - ts_t.headers = {"ts", "n"}; - ts_t.types = {ColumnType::Date, ColumnType::Int}; - std::vector tt = {ts_t}; - auto e = tql_to_sql::emit_sql(st, tt); - check(e.error.empty(), "phase11 sql: granularity ok"); - check(e.sql.find("date_trunc('month'") != std::string::npos && - e.sql.find("SUM(\"n\")") != std::string::npos, - "phase11 sql: date_trunc + SUM"); - } - - // Caso 5: aggregations p25/median/p99 - { - State st; - st.stages.resize(2); - st.stages[1].breakouts.push_back("lang"); - st.stages[1].aggregations.push_back({AggFn::Median, "n"}); - st.stages[1].aggregations.push_back({AggFn::P25, "n"}); - st.stages[1].aggregations.push_back({AggFn::P99, "n"}); - st.active_stage = 1; - auto e = tql_to_sql::emit_sql(st, tables); - check(e.error.empty(), "phase11 sql: percentiles ok"); - check(e.sql.find("quantile_cont(\"n\", 0.5)") != std::string::npos && - e.sql.find("quantile_cont(\"n\", 0.25)") != std::string::npos && - e.sql.find("quantile_cont(\"n\", 0.99)") != std::string::npos, - "phase11 sql: quantile_cont calls"); - } - - // Caso 6: joins 4 strategies - { - State st; - st.stages.resize(1); - Join jn; - jn.alias = "o"; - jn.source = "orders"; - jn.on.push_back({"user_id", "user_id"}); - jn.strategy = JoinStrategy::Left; - st.joins.push_back(jn); - TableInput u, o; - u.name = "users"; - u.headers = {"user_id", "name"}; - u.types = {ColumnType::String, ColumnType::String}; - o.name = "orders"; - o.headers = {"user_id", "amount"}; - o.types = {ColumnType::String, ColumnType::Int}; - std::vector tt = {u, o}; - auto e = tql_to_sql::emit_sql(st, tt); - check(e.error.empty(), "phase11 sql: join ok"); - check(e.sql.find("LEFT JOIN \"orders\" AS \"o\"") != std::string::npos && - e.sql.find("ON \"users\".\"user_id\" = \"o\".\"user_id\"") != std::string::npos, - "phase11 sql: LEFT JOIN ON syntax"); - - // Inner - st.joins[0].strategy = JoinStrategy::Inner; - auto e2 = tql_to_sql::emit_sql(st, tt); - check(e2.sql.find("INNER JOIN") != std::string::npos, "phase11 sql: INNER JOIN"); - - // Right - st.joins[0].strategy = JoinStrategy::Right; - auto e3 = tql_to_sql::emit_sql(st, tt); - check(e3.sql.find("RIGHT JOIN") != std::string::npos, "phase11 sql: RIGHT JOIN"); - - // Full - st.joins[0].strategy = JoinStrategy::Full; - auto e4 = tql_to_sql::emit_sql(st, tt); - check(e4.sql.find("FULL OUTER JOIN") != std::string::npos, "phase11 sql: FULL OUTER JOIN"); - } - - // Caso 7: derived col subset -> SQL expression - { - State st; - st.stages.resize(1); - DerivedColumn d; - d.name = "size_kb"; - d.source_col = -1; - d.formula = "[n] / 1024.0"; - d.type = ColumnType::Float; - st.stages[0].derived.push_back(d); - auto e = tql_to_sql::emit_sql(st, tables); - check(e.error.empty(), "phase11 sql: derived subset ok"); - check(e.sql.find("\"n\" / 1024") != std::string::npos && - e.sql.find("AS \"size_kb\"") != std::string::npos, - "phase11 sql: derived expression + alias"); - } - - // Caso 8: derived col FUERA subset -> warning + skip - { - State st; - st.stages.resize(1); - DerivedColumn d; - d.name = "bad"; - d.source_col = -1; - d.formula = "string.gsub([n], \"a\", \"b\")"; - d.type = ColumnType::String; - st.stages[0].derived.push_back(d); - auto e = tql_to_sql::emit_sql(st, tables); - check(e.error.empty(), "phase11 sql: derived fuera subset NO bloquea emit"); - check(!e.warnings.empty() && - e.warnings[0].find("out of SQL subset") != std::string::npos, - "phase11 sql: warning derived fuera subset"); - check(e.sql.find("\"bad\"") == std::string::npos, - "phase11 sql: derived skip cuando fuera subset"); - } - - // Caso 9: empty tables -> error - { - State st; - st.stages.resize(1); - std::vector empty; - auto e = tql_to_sql::emit_sql(st, empty); - check(!e.error.empty() && e.error.find("no input tables") != std::string::npos, - "phase11 sql: empty tables -> error"); - } - - // Caso 10: stage 0 con LIKE (Contains) - { - State st; - st.stages.resize(1); - st.stages[0].filters.push_back({0, Op::Contains, "go"}); - auto e = tql_to_sql::emit_sql(st, tables); - check(e.error.empty(), "phase11 sql: LIKE Contains ok"); - check(e.sql.find("LIKE ?") != std::string::npos && - e.params.size() == 1 && e.params[0] == "%go%", - "phase11 sql: Contains -> LIKE %go%"); - } - } - - // === phase11: LLM client (mock, no red) === - { - llm_anthropic::AskInput in; - in.question = "show top 10 langs"; - in.tql_current = "return { stages = {} }"; - in.col_names = {"lang", "n"}; - in.col_types = {ColumnType::String, ColumnType::Int}; - in.mode = llm_anthropic::OutputMode::TQL; - std::string body = llm_anthropic::build_request_body(in); - check(body.find("\"model\":\"claude-sonnet-4-6\"") != std::string::npos, - "phase11 llm: default model"); - check(body.find("\"max_tokens\":8192") != std::string::npos, - "phase11 llm: max_tokens"); - check(body.find("\\\"system\\\"") == std::string::npos /* not double-escaped */, - "phase11 llm: system not double-escaped"); - check(body.find("Available columns") != std::string::npos, - "phase11 llm: schema block present"); - check(body.find("show top 10 langs") != std::string::npos, - "phase11 llm: question present"); - check(body.find("TQL") != std::string::npos, - "phase11 llm: system mentions TQL"); - - in.mode = llm_anthropic::OutputMode::SQL; - std::string body_sql = llm_anthropic::build_request_body(in); - check(body_sql.find("DuckDB") != std::string::npos, - "phase11 llm: SQL mode mentions DuckDB"); - } - - { - // extract_code_block - std::string raw1 = "Here you go:\n```lua\nreturn { x = 1 }\n```\nDone!"; - std::string code = llm_anthropic::extract_code_block(raw1, "lua"); - check(code == "return { x = 1 }", "phase11 llm: extract ```lua block"); - - std::string raw2 = "Sure:\n```\nplain code\n```"; - std::string code2 = llm_anthropic::extract_code_block(raw2, "lua"); - check(code2 == "plain code", "phase11 llm: extract bare ```"); - - std::string raw3 = "no fences here"; - std::string code3 = llm_anthropic::extract_code_block(raw3, "lua"); - check(code3 == "no fences here", "phase11 llm: no fence -> stripped"); - - std::string raw4 = "```sql\nSELECT 1;\n```"; - std::string code4 = llm_anthropic::extract_code_block(raw4, "sql"); - check(code4 == "SELECT 1;", "phase11 llm: extract ```sql"); - } - - { - // parse_response_text from JSON - std::string j = "{\"id\":\"x\",\"content\":[{\"type\":\"text\",\"text\":\"hello\\nworld\"}],\"role\":\"assistant\"}"; - std::string t = llm_anthropic::parse_response_text(j); - check(t == "hello\nworld", "phase11 llm: parse text content"); - - std::string j2 = "{\"content\":[{\"type\":\"text\",\"text\":\"\\\"quoted\\\"\"}]}"; - std::string t2 = llm_anthropic::parse_response_text(j2); - check(t2 == "\"quoted\"", "phase11 llm: parse quoted escape"); - - std::string j3 = "{\"error\":\"foo\"}"; - std::string t3 = llm_anthropic::parse_response_text(j3); - check(t3.empty(), "phase11 llm: no text -> empty"); - } - - { - // Mock end-to-end via FN_LLM_MOCK_RESPONSE (portable Linux/Mingw via putenv). - const char* mock_kv = - "FN_LLM_MOCK_RESPONSE={\"content\":[{\"type\":\"text\",\"text\":\"```lua\\nreturn { mock = true }\\n```\"}]}"; - putenv((char*)mock_kv); - llm_anthropic::AskInput in; - in.question = "q"; - in.col_names = {"a"}; - in.col_types = {ColumnType::String}; - auto r = llm_anthropic::ask(in); - check(r.error.empty(), "phase11 llm mock: no error"); - check(r.code == "return { mock = true }", "phase11 llm mock: code extracted"); - // Unset: putenv con "VAR=" deja vacio (suficiente para nuestro check `*mock`). - putenv((char*)"FN_LLM_MOCK_RESPONSE="); - } - -#ifdef FN_TQL_DUCKDB - // === phase11 round-trip TQL emit -> DuckDB execute -> match compute_stage === - { - // Setup tabla "users" con 5 rows. - const char* cells[] = { - "go", "10", - "go", "20", - "py", "30", - "py", "40", - "cpp", "50", - }; - TableInput t; - t.name = "users"; - t.headers = {"lang", "n"}; - t.types = {ColumnType::String, ColumnType::Int}; - t.cells = cells; - t.rows = 5; - t.cols = 2; - std::vector tables = {t}; - - // Caso A: stage 0 simple SELECT - { - State st; - st.stages.resize(1); - auto e = tql_to_sql::emit_sql(st, tables); - check(e.error.empty(), "phase11 rt: stage0 emit OK"); - auto r = tql_duckdb::execute(e.sql, e.params, tables); - check(r.error.empty(), "phase11 rt: stage0 execute OK"); - check(r.out.rows == 5, "phase11 rt: stage0 5 rows"); - check(r.out.cols == 2, "phase11 rt: stage0 2 cols"); - } - - // Caso B: stage 1 group by lang + count - { - State st; - st.stages.resize(2); - st.stages[1].breakouts.push_back("lang"); - st.stages[1].aggregations.push_back({AggFn::Count}); - st.active_stage = 1; - auto e = tql_to_sql::emit_sql(st, tables); - check(e.error.empty(), "phase11 rt: group emit OK"); - auto r = tql_duckdb::execute(e.sql, e.params, tables); - check(r.error.empty(), "phase11 rt: group execute OK"); - check(r.out.rows == 3, "phase11 rt: 3 grupos (go/py/cpp)"); - check(r.out.cols == 2, "phase11 rt: cols = lang + count"); - } - - // Caso C: filter Op::Eq - { - State st; - st.stages.resize(1); - st.stages[0].filters.push_back({0, Op::Eq, "go"}); - auto e = tql_to_sql::emit_sql(st, tables); - check(e.error.empty(), "phase11 rt: filter emit OK"); - auto r = tql_duckdb::execute(e.sql, e.params, tables); - check(r.error.empty(), "phase11 rt: filter execute OK"); - check(r.out.rows == 2, "phase11 rt: filter -> 2 rows go"); - } - - // Caso D: aggregation sum - { - State st; - st.stages.resize(2); - st.stages[1].breakouts.push_back("lang"); - st.stages[1].aggregations.push_back({AggFn::Sum, "n"}); - st.active_stage = 1; - auto e = tql_to_sql::emit_sql(st, tables); - check(e.error.empty(), "phase11 rt: sum emit OK"); - auto r = tql_duckdb::execute(e.sql, e.params, tables); - check(r.error.empty(), "phase11 rt: sum execute OK"); - check(r.out.rows == 3, "phase11 rt: sum 3 grupos"); - // Verifica que sum_n para "go" es 30 (10+20) - bool found_go_30 = false; - for (int rr = 0; rr < r.out.rows; ++rr) { - std::string lang = r.out.cells[rr * 2 + 0]; - std::string sum = r.out.cells[rr * 2 + 1]; - if (lang == "go" && (sum == "30" || sum == "30.0")) found_go_30 = true; - } - check(found_go_30, "phase11 rt: sum_n(go) = 30"); - } - } -#endif - - std::printf("\n=== %d passed, %d failed ===\n", passed, failed); - return failed == 0 ? 0 : 1; -} diff --git a/cpp/apps/primitives_gallery/playground/tables/tql.cpp b/cpp/apps/primitives_gallery/playground/tables/tql.cpp deleted file mode 100644 index ab80f4c4..00000000 --- a/cpp/apps/primitives_gallery/playground/tables/tql.cpp +++ /dev/null @@ -1,913 +0,0 @@ -#include "tql.h" -#include "lua_engine.h" - -extern "C" { -#include "lua.h" -#include "lualib.h" -#include "lauxlib.h" -} - -#include -#include -#include -#include - -namespace tql { - -using namespace data_table; - -namespace { - -int find_orig_col(const std::vector& 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& 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& headers, - const std::vector& 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 eff_headers(eff_cols); - std::vector 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 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& filters, - const std::vector& 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& 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 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 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 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& headers, - const std::vector& /*types*/, - const char* const* cells, int rows, int orig_cols, - std::string* err) -{ - std::vector 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 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 samples_str; - std::vector samples_ptr; - std::vector hn_storage = headers; - std::unordered_map 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 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> order_pairs; - std::vector 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 diff --git a/cpp/apps/primitives_gallery/playground/tables/tql.h b/cpp/apps/primitives_gallery/playground/tables/tql.h deleted file mode 100644 index b06eec77..00000000 --- a/cpp/apps/primitives_gallery/playground/tables/tql.h +++ /dev/null @@ -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 -#include - -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& headers, - const std::vector& 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& headers, - const std::vector& 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 diff --git a/cpp/apps/primitives_gallery/playground/tables/tql_duckdb.cpp b/cpp/apps/primitives_gallery/playground/tables/tql_duckdb.cpp deleted file mode 100644 index 80ed9cde..00000000 --- a/cpp/apps/primitives_gallery/playground/tables/tql_duckdb.cpp +++ /dev/null @@ -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 -#include -#include -#include -#include - -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& params, - const std::vector& 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(t1 - t0).count(); - return out; -} - -} // namespace tql_duckdb - -#endif // FN_TQL_DUCKDB diff --git a/cpp/apps/primitives_gallery/playground/tables/tql_duckdb.h b/cpp/apps/primitives_gallery/playground/tables/tql_duckdb.h deleted file mode 100644 index 792e33ec..00000000 --- a/cpp/apps/primitives_gallery/playground/tables/tql_duckdb.h +++ /dev/null @@ -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 -#include - -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& params, - const std::vector& tables); - -} // namespace tql_duckdb - -#endif // FN_TQL_DUCKDB diff --git a/cpp/apps/runtime_test b/cpp/apps/runtime_test deleted file mode 160000 index 49a9f327..00000000 --- a/cpp/apps/runtime_test +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 49a9f3273d3e8fa39576497da1469476c5a9d9d4 diff --git a/cpp/apps/shaders_lab/.gitignore b/cpp/apps/shaders_lab/.gitignore deleted file mode 100644 index b32bd245..00000000 --- a/cpp/apps/shaders_lab/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -shaders_lab -shaders_lab.exe -build/ -*.zip -operations.db* diff --git a/cpp/apps/shaders_lab/CMakeLists.txt b/cpp/apps/shaders_lab/CMakeLists.txt deleted file mode 100644 index cc25d777..00000000 --- a/cpp/apps/shaders_lab/CMakeLists.txt +++ /dev/null @@ -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() diff --git a/cpp/apps/shaders_lab/NEXT_STEPS_BORDERLESS_WINDOW.md b/cpp/apps/shaders_lab/NEXT_STEPS_BORDERLESS_WINDOW.md deleted file mode 100644 index 5209a2db..00000000 --- a/cpp/apps/shaders_lab/NEXT_STEPS_BORDERLESS_WINDOW.md +++ /dev/null @@ -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. diff --git a/cpp/apps/shaders_lab/NEXT_STEPS_DATA_TYPES.md b/cpp/apps/shaders_lab/NEXT_STEPS_DATA_TYPES.md deleted file mode 100644 index ba00a017..00000000 --- a/cpp/apps/shaders_lab/NEXT_STEPS_DATA_TYPES.md +++ /dev/null @@ -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_(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_` 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_, 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. diff --git a/cpp/apps/shaders_lab/SPEC.md b/cpp/apps/shaders_lab/SPEC.md deleted file mode 100644 index 556ae552..00000000 --- a/cpp/apps/shaders_lab/SPEC.md +++ /dev/null @@ -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; // 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:: `), 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; - 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; // 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; - 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 ``. -- 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 ``. - ---- - -## 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; - thumbnail?: string; // dataURL pequeño para mostrar en SHADERS sidebar - updatedAt: number; - }>; - functions: Record; - 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. diff --git a/cpp/apps/shaders_lab/app.md b/cpp/apps/shaders_lab/app.md deleted file mode 100644 index 3e5ecc26..00000000 --- a/cpp/apps/shaders_lab/app.md +++ /dev/null @@ -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 - `/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`). diff --git a/cpp/apps/shaders_lab/compiler.cpp b/cpp/apps/shaders_lab/compiler.cpp deleted file mode 100644 index 1f2f5d57..00000000 --- a/cpp/apps/shaders_lab/compiler.cpp +++ /dev/null @@ -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 -#include -#include - -// ── 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 g_descs; -extern fn::gfx::UniformStore g_store; -extern std::vector 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 diff --git a/cpp/apps/shaders_lab/compiler.h b/cpp/apps/shaders_lab/compiler.h deleted file mode 100644 index 44d4f3fd..00000000 --- a/cpp/apps/shaders_lab/compiler.h +++ /dev/null @@ -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 diff --git a/cpp/apps/shaders_lab/extracted/ARCHITECTURE.md b/cpp/apps/shaders_lab/extracted/ARCHITECTURE.md deleted file mode 100644 index c7b556dd..00000000 --- a/cpp/apps/shaders_lab/extracted/ARCHITECTURE.md +++ /dev/null @@ -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 - 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, - params: array, 16>, -}; - -@group(0) @binding(0) var u: Uniforms; - -@vertex fn vs(...) { /* fullscreen triangle */ } - -// Una función por nodo, nombrada node_ -fn node_0(c: vec4, uv: vec2) -> vec4 { ... } -fn node_1(a: vec4, b: vec4, uv: vec2) -> vec4 { ... } // blend - -@fragment fn fs(@builtin(position) pos: vec4) -> @location(0) vec4 { - let uv = pos.xy / u.resolution; - let out_0 = node_0(vec4(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_` 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_(uv) -> vec4` - 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` (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`) -- 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= 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`). 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). diff --git a/cpp/apps/shaders_lab/extracted/functional-lab.jsx b/cpp/apps/shaders_lab/extracted/functional-lab.jsx deleted file mode 100644 index 800f352b..00000000 --- a/cpp/apps/shaders_lab/extracted/functional-lab.jsx +++ /dev/null @@ -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 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

; - } - 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 ( - - {showZero && } - - {n <= 40 && pts.map((p, i) => ( - - ))} - - ); -} - -// ───────────────────────────────────────────────────────────────── -// Ficha en la paleta (draggable) -// ───────────────────────────────────────────────────────────────── -function PaletteItem({ fnName, fn, color, onDragStart, onClick }) { - return ( -
{ 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}`} - > - {fn.symbol} - {fnName} - drag -
- ); -} - -// ───────────────────────────────────────────────────────────────── -// 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 ( -
{ 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 }} - > -
- {/* cabecera */} -
- - {def.symbol} - {step.name} - -
- - {/* params */} - {def.params && ( -
- {def.params.map(p => ( - - ))} -
- )} - - {/* preview */} -
- {unreachable ? ( -
inalcanzable · fold anterior terminó el pipeline
- ) : error ? ( -
error: {error}
- ) : isScalar ? ( -
- resultado - {formatScalar(value)} -
- ) : ( -
- -
- n={value?.length ?? 0} · range [{formatScalar(Math.min(...(value?.length?value:[0])))}, {formatScalar(Math.max(...(value?.length?value:[0])))}] -
-
- )} -
-
- - {/* flecha de composición */} - {!isLast && ( -
- )} -
- ); -} - -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 ( -
- arrastra una función para empezar -
- ); - } - const { def, value, error, unreachable } = lastStep; - if (error || unreachable) return null; - const color = CATEGORIES[def.cat].color; - - if (def.cat === 'fold') { - return ( -
- resultado · escalar - {formatScalar(value)} -
- ); - } - - // plot grande - if (!Array.isArray(value) || value.length === 0) { - return
∅ (lista vacía)
; - } - 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 ( -
-
- resultado · señal - n={n} · [{formatScalar(min)}, {formatScalar(max)}] -
- - - - - - - - {/* grid */} - {[0.25, 0.5, 0.75].map(f => ( - - ))} - {showZero && } - - - {n <= 80 && pts.map((p, i) => ( - - ))} - {/* eje */} - - - -
- ); -} - -// ───────────────────────────────────────────────────────────────── -// 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 ( -
- - -
- {/* HEADER */} -
-
-
-
- lab · 001 - / - composición funcional -
-

- laboratorio de morfismos -

-

- arrastra funciones al pipeline y componlas de izquierda a derecha. cada tipo corresponde a un esquema de recursión distinto. -

-
- -
-
- - {/* MAIN GRID · 2 columnas: paleta | (expresión + pipeline + visualizador apilados) */} -
- {/* ─── IZQUIERDA · PALETA ─── */} - - - {/* ─── DERECHA · EXPRESIÓN + PIPELINE + VISUALIZADOR ─── */} -
- {/* Expresión simbólica */} -
-
- expresión - se aplica de derecha a izquierda -
-
- - {expression} - -
-
- - {/* Pipeline */} -
-
- pipeline - {pipeline.length} {pipeline.length === 1 ? 'nodo' : 'nodos'} -
-
{ 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 ? ( -
-
zona de composición
-
arrastra una primitiva desde la izquierda. los nodos se encadenan de izquierda a derecha.
-
- ) : ( -
- {steps.map((step, i) => ( - - ))} -
- )} -
-
- - {/* Visualizador */} -
-
visualizador
-
-
- -
-
-
- - {/* Nota didáctica */} -
- anamorfismo genera estructura · functor map transforma punto a punto ·{' '} - filtro refina · scan acumula dejando rastro ·{' '} - catamorfismo colapsa (es terminal) -
-
-
-
-
- ); -} diff --git a/cpp/apps/shaders_lab/extracted/shader-dag-blends.jsx b/cpp/apps/shaders_lab/extracted/shader-dag-blends.jsx deleted file mode 100644 index c30b7cf2..00000000 --- a/cpp/apps/shaders_lab/extracted/shader-dag-blends.jsx +++ /dev/null @@ -1,1241 +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 nodo declara: -// - kind: 'gen' | 'op' | 'blend' -// - params: hasta 4 floats · slots del vec4 en u.params[idx] -// - controls: descriptores de UI (pueden agrupar varios params) -// - body: snippet WGSL del cuerpo de la función -// ═══════════════════════════════════════════════════════════════════ - -const MAX_NODES = 16; -const ACCENT = '#5eead4'; -const GEN_COLOR = '#5eead4'; -const OP_COLOR = '#c4b5fd'; -const BLEND_COLOR = '#fbbf24'; - -const NODES = { - // ─── GENERADORES ─────────────────────────────────────────────── - solid: { - kind: 'gen', label: 'solid', desc: 'color constante', - params: [ - { k: 'r', d: 0.35 }, { k: 'g', d: 0.25 }, { k: 'b', d: 0.55 }, { k: '_', d: 0 }, - ], - controls: [ - { kind: 'color', keys: ['r', 'g', 'b'], label: 'color' }, - ], - body: (i) => ` - let p = u.params[${i}]; - return vec4(p.x, p.y, p.z, 1.0);`, - }, - - gradient: { - kind: 'gen', label: 'gradient', desc: 'gradiente direccional', - params: [ - { k: 'angle', d: 0.8 }, { k: 'hue', d: 0.5 }, { k: '_', d: 0 }, { k: '_', d: 0 }, - ], - controls: [ - { kind: 'slider', key: 'angle', label: 'ángulo', min: 0, max: 6.2832, step: 0.01 }, - { kind: 'slider', key: 'hue', label: 'tono', min: 0, max: 1, step: 0.01 }, - ], - body: (i) => ` - let p = u.params[${i}]; - let dir = vec2(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(0.0, 0.33, 0.67) + t)); - return vec4(col, 1.0);`, - }, - - plasma: { - kind: 'gen', label: 'plasma', desc: 'onda trigonométrica', - params: [ - { k: 'speed', d: 1 }, { k: 'scale', d: 2 }, { k: '_', d: 0 }, { k: '_', d: 0 }, - ], - controls: [ - { kind: 'slider', key: 'speed', label: 'velocidad', min: 0, max: 3, step: 0.01 }, - { kind: 'slider', key: 'scale', label: 'escala', min: 0.5, max: 10, step: 0.1 }, - ], - body: (i) => ` - let p = u.params[${i}]; - let col = 0.5 + 0.5 * cos(u.time * p.x + uv.xyx * p.y + vec3(0.0, 2.0, 4.0)); - return vec4(col, 1.0);`, - }, - - checker: { - kind: 'gen', label: 'checker', desc: 'tablero rotando', - params: [ - { k: 'scale', d: 8 }, { k: 'rot', d: 0.25 }, { k: '_', d: 0 }, { k: '_', d: 0 }, - ], - controls: [ - { kind: 'slider', key: 'scale', label: 'escala', min: 1, max: 30, step: 0.5 }, - { kind: 'slider', key: 'rot', label: 'rotación', min: -2, max: 2, step: 0.01 }, - ], - body: (i) => ` - let p = u.params[${i}]; - let aspect = u.resolution.x / u.resolution.y; - let q0 = vec2((uv.x - 0.5) * aspect, uv.y - 0.5); - let a = u.time * p.y; - let rm = mat2x2(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(vec3(chk), 1.0);`, - }, - - circle: { - kind: 'gen', label: 'circle', desc: 'sdf de círculo', - params: [ - { k: 'cx', d: 0 }, { k: 'cy', d: 0 }, { k: 'radius', d: 0.35 }, { k: 'soft', d: 0.01 }, - ], - controls: [ - { kind: 'xy', keys: ['cx', 'cy'], label: 'centro', min: -0.8, max: 0.8, step: 0.01 }, - { kind: 'slider', key: 'radius', label: 'radio', min: 0, max: 1, step: 0.01 }, - { kind: 'slider', key: 'soft', label: 'suavidad', min: 0.001, max: 0.1, step: 0.001 }, - ], - body: (i) => ` - let p = u.params[${i}]; - let aspect = u.resolution.x / u.resolution.y; - let pos = vec2((uv.x - 0.5) * aspect - p.x, uv.y - 0.5 - p.y); - let d = length(pos) - p.z; - let fill = smoothstep(p.w, -p.w, d); - return mix(c, vec4(1.0), fill);`, - }, - - stripes: { - kind: 'gen', label: 'stripes', desc: 'rayas animadas', - params: [ - { k: 'freq', d: 20 }, { k: 'speed', d: 1 }, { k: 'angle', d: 0.5 }, { k: '_', d: 0 }, - ], - controls: [ - { kind: 'slider', key: 'freq', label: 'frecuencia', min: 1, max: 80, step: 0.5 }, - { kind: 'slider', key: 'speed', label: 'velocidad', min: -5, max: 5, step: 0.05 }, - { kind: 'slider', key: 'angle', label: 'ángulo', min: 0, max: 3.1416, step: 0.01 }, - ], - body: (i) => ` - let p = u.params[${i}]; - let dir = vec2(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(vec3(v), 1.0);`, - }, - - noise: { - kind: 'gen', label: 'noise', desc: 'hash pseudo-aleatorio', - params: [ - { k: 'scale', d: 80 }, { k: 'seed', d: 7 }, { k: 'anim', d: 0 }, { k: '_', d: 0 }, - ], - controls: [ - { kind: 'slider', key: 'scale', label: 'escala', min: 1, max: 200, step: 1 }, - { kind: 'slider', key: 'seed', label: 'seed', min: 0, max: 100, step: 1 }, - { kind: 'slider', key: 'anim', label: 'animar', min: 0, max: 10, step: 0.1 }, - ], - body: (i) => ` - let p = u.params[${i}]; - let q = floor(uv * p.x + p.y + u.time * p.z); - let h = fract(sin(dot(q, vec2(12.9898, 78.233))) * 43758.5453); - return vec4(vec3(h), 1.0);`, - }, - - // ─── OPERADORES (toman c, devuelven c') ──────────────────────── - invert: { - kind: 'op', label: 'invert', desc: '1 − rgb', - params: [{ k: '_', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }], - controls: [], - body: () => ` - return vec4(1.0 - c.rgb, c.a);`, - }, - - gamma: { - kind: 'op', label: 'gamma', desc: 'pow(rgb, γ)', - params: [{ k: 'g', d: 1 }, { k: '_', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }], - controls: [{ kind: 'slider', key: 'g', label: 'γ', min: 0.1, max: 5, step: 0.01 }], - body: (i) => ` - let g = max(0.01, u.params[${i}].x); - return vec4(pow(max(c.rgb, vec3(0.0)), vec3(g)), c.a);`, - }, - - contrast: { - kind: 'op', label: 'contrast', desc: '(rgb − 0.5)·k + 0.5', - params: [{ k: 'k', d: 1 }, { k: '_', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }], - controls: [{ kind: 'slider', key: 'k', label: 'k', min: 0, max: 3, step: 0.01 }], - body: (i) => ` - return vec4((c.rgb - vec3(0.5)) * u.params[${i}].x + vec3(0.5), c.a);`, - }, - - saturate: { - kind: 'op', label: 'saturate', desc: 'lerp(luma, rgb, s)', - params: [{ k: 's', d: 1 }, { k: '_', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }], - controls: [{ kind: 'slider', key: 's', label: 's', min: 0, max: 2, step: 0.01 }], - body: (i) => ` - let luma = dot(c.rgb, vec3(0.2126, 0.7152, 0.0722)); - return vec4(mix(vec3(luma), c.rgb, u.params[${i}].x), c.a);`, - }, - - hueShift: { - kind: 'op', label: 'hue shift', desc: 'rotar matiz', - params: [{ k: 'h', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }], - controls: [{ kind: 'slider', key: 'h', label: 'h', min: 0, max: 1, step: 0.01 }], - body: (i) => ` - let p = u.params[${i}]; - let k = vec3(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(rot, c.a);`, - }, - - tint: { - kind: 'op', label: 'tint', desc: 'rgb × color', - params: [{ k: 'r', d: 1 }, { k: 'g', d: 0.8 }, { k: 'b', d: 0.7 }, { k: '_', d: 0 }], - controls: [{ kind: 'color', keys: ['r', 'g', 'b'], label: 'tinte' }], - body: (i) => ` - let p = u.params[${i}]; - return vec4(c.rgb * vec3(p.x, p.y, p.z) * 2.0, c.a);`, - }, - - posterize: { - kind: 'op', label: 'posterize', desc: 'cuantizar a N niveles', - params: [{ k: 'levels', d: 5 }, { k: '_', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }], - controls: [{ kind: 'slider', key: 'levels', label: 'niveles', min: 2, max: 16, step: 1 }], - body: (i) => ` - let n = max(2.0, u.params[${i}].x); - return vec4(floor(c.rgb * n) / n, c.a);`, - }, - - vignette: { - kind: 'op', label: 'vignette', desc: 'oscurecer bordes', - params: [{ k: 'cx', d: 0 }, { k: 'cy', d: 0 }, { k: 'strength', d: 1 }, { k: 'radius', d: 0.5 }], - controls: [ - { kind: 'xy', keys: ['cx', 'cy'], label: 'centro', min: -0.5, max: 0.5, step: 0.01 }, - { kind: 'slider', key: 'strength', label: 'fuerza', min: 0, max: 2, step: 0.01 }, - { kind: 'slider', key: 'radius', label: 'radio', min: 0, max: 1.4, step: 0.01 }, - ], - body: (i) => ` - let p = u.params[${i}]; - let d = length((uv - vec2(0.5)) - vec2(p.x, p.y)); - let v = 1.0 - smoothstep(p.w, p.w + 0.3, d) * p.z; - return vec4(c.rgb * v, c.a);`, - }, - - ripple: { - kind: 'op', label: 'ripple', desc: 'ondas radiales', - params: [{ k: 'freq', d: 30 }, { k: 'amp', d: 0.2 }, { k: 'speed', d: 2 }, { k: '_', d: 0 }], - controls: [ - { kind: 'slider', key: 'freq', label: 'frecuencia', min: 1, max: 100, step: 1 }, - { kind: 'slider', key: 'amp', label: 'amplitud', min: 0, max: 1, step: 0.01 }, - { kind: 'slider', key: 'speed', label: 'velocidad', min: -5, max: 5, step: 0.05 }, - ], - body: (i) => ` - let p = u.params[${i}]; - let d = length(uv - vec2(0.5)); - let w = sin(d * p.x - u.time * p.z) * p.y; - return vec4(c.rgb * (1.0 + w), c.a);`, - }, - - pulse: { - kind: 'op', label: 'pulse', desc: 'onda temporal', - params: [{ k: 'freq', d: 2 }, { k: 'amount', d: 0.3 }, { k: '_', d: 0 }, { k: '_', d: 0 }], - controls: [ - { kind: 'slider', key: 'freq', label: 'frecuencia', min: 0, max: 10, step: 0.05 }, - { kind: 'slider', key: 'amount', label: 'cantidad', min: 0, max: 1, step: 0.01 }, - ], - body: (i) => ` - let p = u.params[${i}]; - return vec4(c.rgb * (1.0 + p.y * sin(u.time * p.x)), c.a);`, - }, - - // ─── BLENDS (fan-in: toman a + b + amount) ───────────────────── - blend_mix: { - kind: 'blend', label: 'mix', desc: 'interpolación · mix(a, b, t)', - params: [{ k: 't', d: 0.5 }, { k: '_', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }], - controls: [ - { kind: 'source', label: 'source b' }, - { kind: 'slider', key: 't', label: 't', min: 0, max: 1, step: 0.01 }, - ], - body: (i) => ` - return mix(a, b, u.params[${i}].x);`, - }, - - blend_multiply: { - kind: 'blend', label: 'multiply', desc: 'a · b', - params: [{ k: 'amount', d: 1 }, { k: '_', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }], - controls: [ - { kind: 'source', label: 'source b' }, - { kind: 'slider', key: 'amount', label: 'amount', min: 0, max: 1, step: 0.01 }, - ], - body: (i) => ` - let r = vec4(a.rgb * b.rgb, a.a); - return mix(a, r, u.params[${i}].x);`, - }, - - blend_screen: { - kind: 'blend', label: 'screen', desc: '1 − (1−a)(1−b)', - params: [{ k: 'amount', d: 1 }, { k: '_', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }], - controls: [ - { kind: 'source', label: 'source b' }, - { kind: 'slider', key: 'amount', label: 'amount', min: 0, max: 1, step: 0.01 }, - ], - body: (i) => ` - let r = vec4(1.0 - (1.0 - a.rgb) * (1.0 - b.rgb), a.a); - return mix(a, r, u.params[${i}].x);`, - }, - - blend_add: { - kind: 'blend', label: 'add', desc: 'clamp(a + b)', - params: [{ k: 'amount', d: 1 }, { k: '_', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }], - controls: [ - { kind: 'source', label: 'source b' }, - { kind: 'slider', key: 'amount', label: 'amount', min: 0, max: 1, step: 0.01 }, - ], - body: (i) => ` - let r = vec4(min(a.rgb + b.rgb, vec3(1.0)), a.a); - return mix(a, r, u.params[${i}].x);`, - }, - - blend_difference: { - kind: 'blend', label: 'difference', desc: '|a − b|', - params: [{ k: 'amount', d: 1 }, { k: '_', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }], - controls: [ - { kind: 'source', label: 'source b' }, - { kind: 'slider', key: 'amount', label: 'amount', min: 0, max: 1, step: 0.01 }, - ], - body: (i) => ` - let r = vec4(abs(a.rgb - b.rgb), a.a); - return mix(a, r, u.params[${i}].x);`, - }, - - blend_darken: { - kind: 'blend', label: 'darken', desc: 'min(a, b)', - params: [{ k: 'amount', d: 1 }, { k: '_', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }], - controls: [ - { kind: 'source', label: 'source b' }, - { kind: 'slider', key: 'amount', label: 'amount', min: 0, max: 1, step: 0.01 }, - ], - body: (i) => ` - let r = vec4(min(a.rgb, b.rgb), a.a); - return mix(a, r, u.params[${i}].x);`, - }, - - blend_lighten: { - kind: 'blend', label: 'lighten', desc: 'max(a, b)', - params: [{ k: 'amount', d: 1 }, { k: '_', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }], - controls: [ - { kind: 'source', label: 'source b' }, - { kind: 'slider', key: 'amount', label: 'amount', min: 0, max: 1, step: 0.01 }, - ], - body: (i) => ` - let r = vec4(max(a.rgb, b.rgb), a.a); - return mix(a, r, u.params[${i}].x);`, - }, - - blend_mask: { - kind: 'blend', label: 'mask', desc: 'usa luma(b) como alpha', - params: [{ k: 'invert', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }, { k: '_', d: 0 }], - controls: [ - { kind: 'source', label: 'source b (máscara)' }, - { kind: 'select', key: 'invert', label: 'invertir', options: ['no', 'sí'] }, - ], - body: (i) => ` - let luma = dot(b.rgb, vec3(0.2126, 0.7152, 0.0722)); - let m = select(luma, 1.0 - luma, u.params[${i}].x > 0.5); - return vec4(a.rgb * m, a.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 })), - blend: Object.entries(NODES).filter(([, v]) => v.kind === 'blend').map(([k, v]) => ({ name: k, ...v })), -}; - -// ═══════════════════════════════════════════════════════════════════ -// Compilador: DAG → WGSL (con soporte para blends / fan-in) -// ═══════════════════════════════════════════════════════════════════ - -function compileDagToWGSL(pipeline) { - const safe = pipeline.slice(0, MAX_NODES); - - const fns = safe.map((step, idx) => { - const def = NODES[step.name]; - const sig = def.kind === 'blend' - ? '(a: vec4, b: vec4, uv: vec2)' - : '(c: vec4, uv: vec2)'; - return `fn node_${idx}${sig} -> vec4 {${def.body(idx)} -}`; - }).join('\n\n'); - - let chain; - if (safe.length === 0) { - chain = ' var c = vec4(0.04, 0.04, 0.06, 1.0);'; - } else { - const lines = safe.map((step, idx) => { - const def = NODES[step.name]; - const prev = idx === 0 ? 'vec4(0.0, 0.0, 0.0, 1.0)' : `out_${idx - 1}`; - if (def.kind === 'blend') { - // resolver source por id (robusto ante reordenamientos) - let srcIdx = -1; - const srcId = step.meta?.sourceId; - if (srcId) { - srcIdx = safe.findIndex(s => s.id === srcId); - } - if (srcIdx < 0 || srcIdx >= idx) srcIdx = Math.max(0, idx - 2); // fallback: el de hace dos - const src = idx === 0 ? prev : `out_${Math.min(srcIdx, idx - 1)}`; - return ` let out_${idx} = node_${idx}(${prev}, ${src}, uv);`; - } - return ` let out_${idx} = node_${idx}(${prev}, uv);`; - }); - chain = lines.join('\n'); - } - - const finalExpr = safe.length === 0 ? 'c' : `out_${safe.length - 1}`; - - return `struct Uniforms { - time: f32, - _pad: f32, - resolution: vec2, - params: array, ${MAX_NODES}>, -}; - -@group(0) @binding(0) var u: Uniforms; - -@vertex -fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4 { - let p = array, 3>( - vec2(-1.0, -1.0), - vec2( 3.0, -1.0), - vec2(-1.0, 3.0) - ); - return vec4(p[i], 0.0, 1.0); -} - -${fns} - -@fragment -fn fs(@builtin(position) pos: vec4) -> @location(0) vec4 { - let uv = pos.xy / u.resolution; -${chain} - return ${finalExpr}; -} -`; -} - -// ═══════════════════════════════════════════════════════════════════ -// Uniform buffer -// ═══════════════════════════════════════════════════════════════════ - -const UNIFORM_FLOATS = 4 + MAX_NODES * 4; -const UNIFORM_BYTES = UNIFORM_FLOATS * 4; - -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 && p.k !== '_' ? (step.params[p.k] ?? p.d) : (p ? p.d : 0); - } - } - device.queue.writeBuffer(buffer, 0, data); -} - -// ═══════════════════════════════════════════════════════════════════ -// Hook WebGPU + 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); - - 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]); - - 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]); - - 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]; - const obj = {}; - for (const p of d.params) if (p.k !== '_') obj[p.k] = p.d; - return obj; -} - -// ═══════════════════════════════════════════════════════════════════ -// Controles UI -// ═══════════════════════════════════════════════════════════════════ - -function ParamSlider({ ctrl, value, onChange, color }) { - const display = ctrl.step >= 1 ? Math.round(value) : Number(value).toFixed(ctrl.step < 0.01 ? 3 : 2); - return ( -
- {ctrl.label} - onChange(Number(e.target.value))} - onPointerDown={(e) => e.stopPropagation()} - className="flex-1" - style={{ accentColor: color, minWidth: '60px' }} - /> - {display} -
- ); -} - -function XYPad({ ctrl, xValue, yValue, onChange, color }) { - const ref = useRef(null); - const [dragging, setDragging] = useState(false); - - const xNorm = (xValue - ctrl.min) / (ctrl.max - ctrl.min); - const yNorm = (yValue - ctrl.min) / (ctrl.max - ctrl.min); - - const update = useCallback((clientX, clientY) => { - const el = ref.current; - if (!el) return; - const rect = el.getBoundingClientRect(); - const nx = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); - const ny = Math.max(0, Math.min(1, 1 - (clientY - rect.top) / rect.height)); - const xv = ctrl.min + nx * (ctrl.max - ctrl.min); - const yv = ctrl.min + ny * (ctrl.max - ctrl.min); - onChange(xv, yv); - }, [ctrl.min, ctrl.max, onChange]); - - useEffect(() => { - if (!dragging) return; - const onMove = (e) => update(e.clientX, e.clientY); - const onUp = () => setDragging(false); - window.addEventListener('pointermove', onMove); - window.addEventListener('pointerup', onUp); - return () => { - window.removeEventListener('pointermove', onMove); - window.removeEventListener('pointerup', onUp); - }; - }, [dragging, update]); - - return ( -
-
- {ctrl.label} - - {xValue.toFixed(2)} , {yValue.toFixed(2)} - -
-
{ - e.stopPropagation(); - e.preventDefault(); - setDragging(true); - update(e.clientX, e.clientY); - }} - className="relative rounded cursor-crosshair select-none" - style={{ - width: '100%', - height: '70px', - background: `${color}0a`, - border: `1px solid ${color}30`, - }} - > -
-
-
-
-
- ); -} - -function ColorPicker({ ctrl, r, g, b, onChange, color }) { - const toHex = () => { - const h = (v) => Math.round(Math.max(0, Math.min(1, v)) * 255).toString(16).padStart(2, '0'); - return `#${h(r)}${h(g)}${h(b)}`; - }; - const fromHex = (hex) => { - const rr = parseInt(hex.slice(1, 3), 16) / 255; - const gg = parseInt(hex.slice(3, 5), 16) / 255; - const bb = parseInt(hex.slice(5, 7), 16) / 255; - return [rr, gg, bb]; - }; - return ( -
- {ctrl.label} -