chore: auto-commit (286 archivos)

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 16:33:22 +02:00
parent 0b9af8f1bb
commit a03675113a
281 changed files with 12596 additions and 19526 deletions
+10
View File
@@ -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. 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. 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. 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_<issue>_<ts>/`. 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.
--- ---
+131
View File
@@ -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-<slug>.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 <id>` 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 <slug> # nuevo flow desde template, ID auto
/flow list # tabla resumen
/flow show <NNNN> # imprime contenido + acceptance %
/flow status <NNNN> # status + acceptance % + ultima run
/flow done <NNNN> [--notes "..."] # cierra flow (status=done, mueve a completed/)
/flow run <NNNN> # fase 2 — runner automatizado (NO IMPLEMENTADO)
```
## Implementacion por subcomando
### `create <slug>`
Pasos:
1. Valida `<slug>` es kebab-case: `^[a-z][a-z0-9-]*$`. Si no, error.
2. Comprueba que no existe ya: `ls dev/flows/*-<slug>.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 `<slug>`, `NNNN`, `YYYY-MM-DD` (hoy).
6. Escribe `dev/flows/NNNN-<slug>.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 <name>` 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 <NNNN>`
`cat dev/flows/NNNN-*.md` (busca con glob NNNN-*). Si no existe, prueba `dev/flows/completed/NNNN-*.md`. Si no, error.
### `status <NNNN>`
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 <NNNN> [--notes "..."]`
Pasos:
1. Verifica todos los `[ ]` estan checked. Si no, prompt "X checks pendientes, --force para cerrar igualmente".
2. Edita frontmatter: `status: done`, `updated: <hoy>`.
3. Si `--notes`, append a seccion `## Notas`.
4. `git mv dev/flows/NNNN-<slug>.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 <NNNN>` — 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: <id>` -> ejecuta `./fn run <id>`.
- Cada paso tipo `cmd: <bash>` -> ejecuta subprocess.
- Texto libre -> "MANUAL: <text>" + pause user input.
- Persistencia ejecuciones en `dev/flows/runs/<id>-<timestamp>.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
```
+14
View File
@@ -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 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" "<fn_id>" "<one-line purpose>"
```
El script es idempotente (si la fn ya esta linkeada, no duplica). Crea `reference_fn_<id>.md` con metadata `type: reference` e indexa la entrada en `MEMORY.md` como linea `- [fn-<id>](reference_fn_<id>.md) — <purpose>`. 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: Reporta:
- N funciones nuevas creadas (con IDs) - N funciones nuevas creadas (con IDs)
- N proposals nuevas en `registry.db.proposals` - N proposals nuevas en `registry.db.proposals`
+1
View File
@@ -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 | | 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/<grupo>.md` para desbloquear clusters de funciones en un read. Issue 0086 | | 29 | [capability_groups.md](capability_groups.md) | Tags planos + paginas madre `docs/capabilities/<grupo>.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 | | 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 |
+75
View File
@@ -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/<issue>`, NUNCA merge a master.
### Cuando se invoca
- Skill `/autonomous-task <issue_id>` (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/<issue_id>-<slug>`. 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 <id>` 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 <main_repo> 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/<issue>` 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.
+162 -12
View File
@@ -20,14 +20,14 @@ Razones:
Pipeline: `init_cpp_app_bash_pipelines`. Slash command equivalente: `/new-cpp-app`. Auditoria: `fn doctor cpp-apps`. 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 | | Caso | Donde vive |
|---|---| |---|---|
| App independiente | `cpp/apps/<nombre>/` | | App independiente | `apps/<nombre>/` |
| App de un proyecto | `projects/<proyecto>/apps/<nombre>/` | | App de un proyecto | `projects/<proyecto>/apps/<nombre>/` |
NUNCA en `cpp/apps/<nombre>/` si pertenece a un proyecto, NUNCA fuera de `apps/` directamente. Ver `apps_location` en memoria + regla `apps_vs_functions.md`. NUNCA en `cpp/apps/<nombre>/` (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 ### 2. Estructura minima
@@ -189,20 +189,105 @@ WMs). Activado por defecto, sin opt-in:
con `glfwSetWindowPos/Size` (no espera al siguiente NewFrame). con `glfwSetWindowPos/Size` (no espera al siguiente NewFrame).
2. **Per-frame viewport sync** al inicio del main loop — cubre viewports 2. **Per-frame viewport sync** al inicio del main loop — cubre viewports
secundarios (paneles drag-out) que la backend crea dinamicamente. secundarios (paneles drag-out) que la backend crea dinamicamente.
3. **Win32 WndProc subclass** (`#ifdef _WIN32`) — observa `WM_ENTERSIZEMOVE` 3. **Win32 WndProc subclass per HWND** (`#ifdef _WIN32`) — observa
/ `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada drag. Mientras `WM_ENTERSIZEMOVE` / `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada
el bracket esta abierto el main loop SKIPEA `render_fn` + `glfwSwapBuffers`, drag. El subclass se instala en la ventana principal Y en cada HWND
replicando el contrato del title-bar drag native (DefWindowProc bloquea secundario que el backend de ImGui crea cuando un panel se arrastra fuera
el hilo, DWM compositor mueve el framebuffer existente). 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<HWND, WNDPROC>`. 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 - `p1.sync` (cross-platform): drives `glfwSetWindowPos` cada frame, asserta
`vp->Pos` sigue OS dentro de 1px. `vp->Pos` sigue OS dentro de 1px.
- `p2.altsnap` (Windows): worker thread fakea `WM_ENTERSIZEMOVE` + - `p2.altsnap` (Windows): worker thread fakea `WM_ENTERSIZEMOVE` +
burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE`, asserta burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE` sobre el
que `render()` no se llama durante el bracket. 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 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 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`. - App headless / capture mode: `cfg.auto_layouts = false`.
- Cambiar nombre del archivo: `cfg.auto_layouts_db = "<algo>.db"` (relativo a - Cambiar nombre del archivo: `cfg.auto_layouts_db = "<algo>.db"` (relativo a
`local_files/`). `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
`<app_dir>/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 `<CMAKE_CURRENT_SOURCE_DIR>/appicon.ico`, genera un
`<target>_appicon.rc` en `CMAKE_CURRENT_BINARY_DIR` apuntando al `.ico` con
`IDI_ICON1 ICON "<path>"` 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 <app>.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 <keyword>` 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.
+34 -2
View File
@@ -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 `<noun>_<modifier>`. 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.
```
+53
View File
@@ -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 <fn_id> "<one-line purpose>"
set -euo pipefail
FN_ID="${1:-}"
PURPOSE="${2:-}"
if [ -z "$FN_ID" ] || [ -z "$PURPOSE" ]; then
echo "usage: append_fn_to_memory.sh <fn_id> <purpose>" >&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" <<EOF
---
name: fn-$FN_ID
description: Registry function $FN_ID — $PURPOSE
metadata:
type: reference
---
Registry function: \`$FN_ID\`
$PURPOSE
Invoke via \`./fn run $FN_ID [args]\` or \`mcp__registry__fn_run id="$FN_ID"\`. Inspect with \`mcp__registry__fn_show id="$FN_ID"\` / \`mcp__registry__fn_code id="$FN_ID"\`.
EOF
# 2. Append index line to MEMORY.md
printf -- '- [%s](%s) — %s\n' "fn-$FN_ID" "$SLUG" "$PURPOSE" >> "$MEM_FILE"
echo "appended: $FN_ID -> $MEM_FILE"
+29
View File
@@ -8,6 +8,35 @@ Para contexto detallado del trabajo diario ver `docs/diary/`. Para decisiones ar
## [Unreleased] ## [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 `<exe_dir>` 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 `<app_dir>/appicon.ico` y genera `<target>_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<HWND, WNDPROC>`, 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/<X>/` (canonical issue 0096) ademas de `cpp/apps/<X>/` (legacy) y `projects/*/apps/<X>/`. Fix retroactivo: `./fn run compile_cpp_app <name>` 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 ## 2026-05-14
### Added ### Added
+4
View File
@@ -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
@@ -6,16 +6,21 @@
scan_secrets_in_dirty() { scan_secrets_in_dirty() {
local repo_dir="${1:-.}" 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 echo "scan_secrets_in_dirty: '$repo_dir' no es un repo git" >&2
return 1 return 1
fi fi
# Listar archivos modificados o nuevos (excluyendo borrados) # 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 \ git -C "$repo_dir" status --porcelain \
| awk '{print $NF}' \ | awk '{print $NF}' \
| grep -E '(^|/)(\.env(\..*)?$|.*credentials.*|.*\.key$|.*\.pem$|id_rsa.*|.*secret.*|.*token.*\.txt$)' \ | 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 || true
} }
@@ -17,7 +17,9 @@ git_hook_audit_app_drift() {
echo "ERROR: repo_dir required" >&2 echo "ERROR: repo_dir required" >&2
return 2 return 2
fi 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 echo "ERROR: $repo_dir is not a git repo" >&2
return 2 return 2
fi fi
+14 -5
View File
@@ -3,11 +3,11 @@ name: resolve_cpp_app_dir
kind: function kind: function
lang: bash lang: bash
domain: infra domain: infra
version: "1.0.0" version: "1.1.0"
purity: impure purity: impure
signature: "resolve_cpp_app_dir(app_name?: string) -> stdout: app_name\tapp_dir" 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/<X>/ o projects/*/apps/<X>/. Con arg busca en ambas ubicaciones. Imprime '<app_name>TAB<absolute_dir>' en stdout, exit 0; si no resuelve, lista apps disponibles en stderr y sale con exit 1." description: "Resuelve el nombre y directorio absoluto de una app C++ del registry. Sin arg deduce desde CWD si esta dentro de apps/<X>/, cpp/apps/<X>/ o projects/*/apps/<X>/. Con arg busca en las tres ubicaciones (apps/ canonical issue 0096 primero, luego cpp/apps/ legacy, luego projects/*/apps/). Imprime '<app_name>TAB<absolute_dir>' en stdout, exit 0; si no resuelve, lista apps disponibles en stderr y sale con exit 1."
tags: [cpp, resolve, app, directory, infra] tags: [cpp, resolve, app, directory, infra, cpp-windows]
uses_functions: [] uses_functions: []
uses_types: [] uses_types: []
returns: [] returns: []
@@ -20,7 +20,7 @@ test_file_path: ""
file_path: "bash/functions/infra/resolve_cpp_app_dir.sh" file_path: "bash/functions/infra/resolve_cpp_app_dir.sh"
params: params:
- name: app_name - 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/<X>/ o projects/*/apps/<X>/." desc: "Nombre de la app C++ a resolver (opcional). Sin arg se deduce desde el directorio actual si estamos dentro de apps/<X>/, cpp/apps/<X>/ o projects/*/apps/<X>/."
output: "Una linea TAB-separada '<app_name>\\t<absolute_dir_path>' en stdout. En caso de error imprime ayuda a stderr y sale con exit 1." output: "Una linea TAB-separada '<app_name>\\t<absolute_dir_path>' 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 ## Notas
Busca en orden: primero `$ROOT/cpp/apps/<X>`, luego `$ROOT/projects/*/apps/<X>` (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/<X>` con `CMakeLists.txt` — layout canonical post-issue 0096.
2. `$ROOT/cpp/apps/<X>` — legacy pre-issue 0096.
3. `$ROOT/projects/*/apps/<X>` — 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/<X>/` (canonical issue 0096). Antes solo cubria `cpp/apps/<X>/` y `projects/*/apps/<X>/`, lo que hacia que `./fn run compile_cpp_app <name>` fallara para apps movidas al layout canonical (ej. `dag_engine_ui`).
+25 -20
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# resolve_cpp_app_dir — Resuelve nombre y directorio absoluto de una app C++ del registry. # 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/<X>/ o projects/*/apps/<X>/. # Sin arg: deduce desde CWD si esta dentro de apps/<X>/, cpp/apps/<X>/ o projects/*/apps/<X>/.
# Con arg: usa el nombre directamente y busca en ambas ubicaciones. # Con arg: usa el nombre directamente y busca en las tres ubicaciones.
# Salida: "<app_name>\t<absolute_dir_path>" en stdout (TAB separado), exit 0. # Salida: "<app_name>\t<absolute_dir_path>" en stdout (TAB separado), exit 0.
# Error: lista apps disponibles en stderr + exit 1. # Error: lista apps disponibles en stderr + exit 1.
@@ -9,18 +9,28 @@ resolve_cpp_app_dir() {
local app_arg="${1:-}" local app_arg="${1:-}"
local root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}" 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 --- # --- Deducir desde CWD si no hay argumento ---
if [ -z "$app_arg" ]; then if [ -z "$app_arg" ]; then
local cwd local cwd
cwd="$(pwd)" cwd="$(pwd)"
case "$cwd" in case "$cwd" in
"$root"/apps/*/|"$root"/apps/*)
local rel="${cwd#"$root/apps/"}"
app_arg="${rel%%/*}"
;;
"$root"/cpp/apps/*/|"$root"/cpp/apps/*) "$root"/cpp/apps/*/|"$root"/cpp/apps/*)
# Extraer primer segmento tras cpp/apps/
local rel="${cwd#"$root/cpp/apps/"}" local rel="${cwd#"$root/cpp/apps/"}"
app_arg="${rel%%/*}" app_arg="${rel%%/*}"
;; ;;
"$root"/projects/*/apps/*/|"$root"/projects/*/apps/*) "$root"/projects/*/apps/*/|"$root"/projects/*/apps/*)
# Extraer primer segmento tras la ultima /apps/
local rel="${cwd#"$root/projects/"}" local rel="${cwd#"$root/projects/"}"
rel="${rel#*/apps/}" rel="${rel#*/apps/}"
app_arg="${rel%%/*}" 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 "ERROR: no se pudo deducir la app desde el directorio actual." >&2
echo "" >&2 echo "" >&2
echo "Apps disponibles:" >&2 echo "Apps disponibles:" >&2
{ _list_cpp_apps >&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
echo "" >&2 echo "" >&2
echo "Uso: resolve_cpp_app_dir <app_name>" >&2 echo "Uso: resolve_cpp_app_dir <app_name>" >&2
return 1 return 1
@@ -47,12 +52,17 @@ resolve_cpp_app_dir() {
# --- Buscar directorio real --- # --- Buscar directorio real ---
local app_dir="" local app_dir=""
# Primero: cpp/apps/<X> # Primero (issue 0096 canonical): apps/<X>
if [ -d "$root/cpp/apps/$app_arg" ]; then 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/<X>
if [ -z "$app_dir" ] && [ -d "$root/cpp/apps/$app_arg" ]; then
app_dir="$root/cpp/apps/$app_arg" app_dir="$root/cpp/apps/$app_arg"
fi fi
# Segundo: projects/*/apps/<X> (primer match) # Tercero: projects/*/apps/<X> (primer match)
if [ -z "$app_dir" ]; then if [ -z "$app_dir" ]; then
for cand in "$root"/projects/*/apps/"$app_arg"; do for cand in "$root"/projects/*/apps/"$app_arg"; do
if [ -d "$cand" ]; then if [ -d "$cand" ]; then
@@ -63,15 +73,10 @@ resolve_cpp_app_dir() {
fi fi
if [ -z "$app_dir" ]; then 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 "" >&2
echo "Apps disponibles:" >&2 echo "Apps disponibles:" >&2
{ _list_cpp_apps >&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
return 1 return 1
fi fi
@@ -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|<args>...]"
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.
@@ -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
+8 -5
View File
@@ -8,7 +8,7 @@
# Uso: # Uso:
# init_cpp_app <name> [--project <p>] [--domain <d>] [--desc "..."] [--tags "a,b"] # init_cpp_app <name> [--project <p>] [--domain <d>] [--desc "..."] [--tags "a,b"]
# #
# Por defecto domain=tools, sin proyecto (cpp/apps/<name>/). # Por defecto domain=tools, sin proyecto (apps/<name>/, issue 0096).
set -euo pipefail set -euo pipefail
@@ -55,7 +55,7 @@ init_cpp_app() {
fi fi
rel_dir="projects/$project/apps/$name" rel_dir="projects/$project/apps/$name"
else else
rel_dir="cpp/apps/$name" rel_dir="apps/$name"
fi fi
abs_dir="$FN_ROOT/$rel_dir" abs_dir="$FN_ROOT/$rel_dir"
@@ -201,11 +201,14 @@ if(EXISTS \${_${upper}_DIR}/CMakeLists.txt)
endif() endif()
EOF EOF
else else
local upper
upper="$(echo "$name" | tr '[:lower:]' '[:upper:]')"
cat >> "$cpp_cmake" <<EOF cat >> "$cpp_cmake" <<EOF
# --- $name --- # --- $name (lives in apps/, issue 0096) ---
if(EXISTS \${CMAKE_CURRENT_SOURCE_DIR}/apps/$name/CMakeLists.txt) set(_${upper}_DIR \${CMAKE_SOURCE_DIR}/../apps/$name)
add_subdirectory(apps/$name) if(EXISTS \${_${upper}_DIR}/CMakeLists.txt)
add_subdirectory(\${_${upper}_DIR} \${CMAKE_BINARY_DIR}/apps/$name)
endif() endif()
EOF EOF
fi fi
@@ -0,0 +1,92 @@
---
name: redeploy_all_cpp_apps
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
purity: impure
signature: "redeploy_all_cpp_apps(filter?: string) -> 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 <name> <dir>`
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/<name>/<name>.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 <name>.exe /F` silencioso (no aborta si falla).
- `deploy_cpp_exe_to_windows <name> <app_dir>` (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/<X>/`) 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/<name>/<name>.exe`. Lanzar individualmente con `./fn run is_cpp_app_running_windows <name>` para chequear y `launch_cpp_app_windows <name>` para arrancar.
@@ -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
+26
View File
@@ -59,6 +59,8 @@ func cmdDoctor(args []string) {
} else { } else {
doctorCapabilities(r, jsonOut) doctorCapabilities(r, jsonOut)
} }
case "app-location":
doctorAppLocation(r, jsonOut)
default: default:
fmt.Fprintf(os.Stderr, "unknown doctor subcommand: %s\n", sub) fmt.Fprintf(os.Stderr, "unknown doctor subcommand: %s\n", sub)
doctorUsage() doctorUsage()
@@ -84,6 +86,7 @@ Subcommands:
vaults Salud de vaults: directorio, layout, índice, staleness, drift vaults Salud de vaults: directorio, layout, índice, staleness, drift
copied-code Detecta cuerpos de funcion del registry copiados en apps sin import (issue 0085k) 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 <grupo>.md (issue 0086) capabilities Drift entre docs/capabilities/INDEX.md, tags de funciones, y paginas <grupo>.md (issue 0086)
app-location Detecta artefactos (apps/analysis) en carpetas de lenguaje (cpp/apps/, etc.) - issue 0096
Flags: Flags:
--json Salida JSON (para scripting/agentes) --json Salida JSON (para scripting/agentes)
@@ -513,3 +516,26 @@ func doctorCopiedCode(root string, jsonOut bool) {
w.Flush() w.Flush()
fmt.Printf("\n%d suspected copy match(es).\n", len(entries)) 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/<name>/ or projects/<p>/apps/<name>/ (issue 0096).\n", len(violations))
}
+71 -32
View File
@@ -1,5 +1,9 @@
cmake_minimum_required(VERSION 3.16) 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 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) 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") set(FN_CPP_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE INTERNAL "fn_registry cpp root")
function(add_imgui_app target) function(add_imgui_app target)
add_executable(${target} ${ARGN}) # Windows icon: si la app tiene <app_dir>/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_link_libraries(${target} PRIVATE fn_framework)
target_include_directories(${target} PRIVATE target_include_directories(${target} PRIVATE
${FN_CPP_ROOT_DIR}/functions ${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) target_link_libraries(fn_table_viz PRIVATE fn_framework)
endif() endif()
# --- Demo app --- # --- Demo app (lives in apps/, issue 0096 standardization) ---
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/chart_demo/CMakeLists.txt) if(NOT DEFINED _CHART_DEMO_DIR)
add_subdirectory(apps/chart_demo) 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() endif()
# --- Shaders Lab --- # --- Shaders Lab (lives in apps/) ---
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/shaders_lab/CMakeLists.txt) if(NOT DEFINED _SHADERS_LAB_DIR)
add_subdirectory(apps/shaders_lab) 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() endif()
# --- Lua 5.4 vendored (para playground tables / DSL formulas) --- # --- 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) add_subdirectory(vendor/lua)
endif() endif()
# --- Primitives Gallery (catalogo visual de primitivos core/viz/gfx) --- # --- Primitives Gallery (lives in apps/) ---
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/primitives_gallery/CMakeLists.txt) if(NOT DEFINED _PG_DIR)
add_subdirectory(apps/primitives_gallery) 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() endif()
# --- Tables playground (vive dentro de primitives_gallery/playground/tables/) --- # --- Tables playground (vive dentro de primitives_gallery/playground/tables/) ---
# No es un app del registry; sirve para iterar mejoras sobre table_view_cpp_viz if(EXISTS ${_PG_DIR}/playground/tables/CMakeLists.txt)
# antes de promover una API v2 y migrar las apps C++ que hoy usan ImGui::BeginTable raw. add_subdirectory(${_PG_DIR}/playground/tables ${CMAKE_BINARY_DIR}/apps/primitives_gallery/playground/tables)
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/primitives_gallery/playground/tables/CMakeLists.txt)
add_subdirectory(apps/primitives_gallery/playground/tables)
endif() endif()
# --- text_editor + file_watcher smoke test (issue 0025) --- # --- text_editor + file_watcher smoke test (lives in apps/) ---
# Build gate para validar que text_editor.cpp + file_watcher.cpp + vendor enlazan. if(NOT DEFINED _TES_DIR)
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/text_editor_smoke/CMakeLists.txt) set(_TES_DIR ${CMAKE_SOURCE_DIR}/../apps/text_editor_smoke)
add_subdirectory(apps/text_editor_smoke) endif()
if(EXISTS ${_TES_DIR}/CMakeLists.txt)
add_subdirectory(${_TES_DIR} ${CMAKE_BINARY_DIR}/apps/text_editor_smoke)
endif() endif()
# --- AltSnap viewport-jitter regression test --- # --- AltSnap viewport-jitter regression test (lives in apps/) ---
# Headless harness que conduce glfwSetWindowPos cada frame y verifica que if(NOT DEFINED _AJT_DIR)
# ImGui viewport->Pos sigue al OS dentro de 1px. Sin la patch del framework set(_AJT_DIR ${CMAKE_SOURCE_DIR}/../apps/altsnap_jitter_test)
# (callback GLFW + per-frame sync) este test falla exit=1. endif()
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/altsnap_jitter_test/CMakeLists.txt) if(EXISTS ${_AJT_DIR}/CMakeLists.txt)
add_subdirectory(apps/altsnap_jitter_test) add_subdirectory(${_AJT_DIR} ${CMAKE_BINARY_DIR}/apps/altsnap_jitter_test)
endif() endif()
# --- gamedev stack (SDL3 + sokol_gfx + miniaudio, issue 0072) --- # --- 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_INSTALL OFF CACHE BOOL "" FORCE)
set(SDL_X11_XSCRNSAVER OFF CACHE BOOL "" FORCE) set(SDL_X11_XSCRNSAVER OFF CACHE BOOL "" FORCE)
add_subdirectory(vendor/sdl3 EXCLUDE_FROM_ALL) add_subdirectory(vendor/sdl3 EXCLUDE_FROM_ALL)
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/engine_smoke/CMakeLists.txt) if(NOT DEFINED _ES_DIR)
add_subdirectory(apps/engine_smoke) set(_ES_DIR ${CMAKE_SOURCE_DIR}/../apps/engine_smoke)
endif() endif()
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/runtime_test/CMakeLists.txt) if(EXISTS ${_ES_DIR}/CMakeLists.txt)
add_subdirectory(apps/runtime_test) 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()
endif() endif()
@@ -421,7 +451,16 @@ if(BUILD_TESTING)
endif() endif()
# --- dag_engine_ui --- # --- dag_engine_ui (lives in apps/, issue 0096) ---
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/dag_engine_ui/CMakeLists.txt) if(NOT DEFINED _DAG_UI_DIR)
add_subdirectory(apps/dag_engine_ui) 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() endif()
Submodule cpp/apps/altsnap_jitter_test deleted from 6e52b658a3
-22
View File
@@ -1,22 +0,0 @@
add_imgui_app(chart_demo
main.cpp
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
# fps_overlay vive en fn_framework
)
# --- E2E tests (opt-in via -DFN_BUILD_TESTS=ON) ---
if(FN_BUILD_TESTS)
add_imgui_app(chart_demo_tests
main.cpp
tests/chart_demo_tests.cpp
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
)
# Excludes int main() from main.cpp so the test target provides its own.
target_compile_definitions(chart_demo_tests PRIVATE FN_TEST_BUILD)
endif()
-60
View File
@@ -1,60 +0,0 @@
---
name: chart_demo
lang: cpp
domain: viz
description: "Demo ImGui de primitivos viz del registry: line_plot, scatter_plot, bar_chart, heatmap. Cada chart en su propia tab del TabBar. Usado como showcase y como build gate de las funciones viz/."
tags: [imgui, demo, charts, viz, showcase]
uses_functions:
- line_plot_cpp_viz
- scatter_plot_cpp_viz
- bar_chart_cpp_viz
- heatmap_cpp_viz
# logger, app_menubar viven en fn_framework — no se listan aqui
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/chart_demo"
repo_url: ""
---
## Que hace
App de una sola ventana con cuatro tabs (Line / Scatter / Bar / Heatmap) que
renderiza datos sinteticos para mostrar el aspecto y la API de los primitivos
viz del registry. Sirve como:
- **Showcase visual** de las funciones viz existentes — al añadir una nueva
primitiva, anadir su tab aqui es la forma natural de probar el binding.
- **Build gate**: si una de las funciones rompe API, esta app deja de
compilar y lo cazamos sin tener que tocar `registry_dashboard` o
`graph_explorer`.
## Estructura
`main.cpp` (~93 lineas):
- `init_data()` — genera arrays sinteticos una vez (estado modulo).
- `render()` — DockSpaceOverViewport + TabBar con 4 tabs, cada una invoca
un primitivo del registry.
- `main()``fn::run_app(...)` con AppConfig estandar (titulo, tamaño,
about, log).
## Build
```bash
# Linux
cd cpp && cmake -B build/linux -S . && cmake --build build/linux --target chart_demo
# Windows (cross-compile)
cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w64.cmake \
&& cmake --build build/windows --target chart_demo
```
## Decisiones
- `viewports = true` (default de `fn::run_app`): las ventanas se pueden
arrastrar fuera del main window.
- `init_gl_loader = false`: solo usa ImGui/ImPlot, sin gl* directo.
- Sin persistencia propia (no abre BD).
- `log: file_path = "chart_demo.log"` con nivel Debug — el `init_data`
emite info+debug para verificar que el logger funciona.
-89
View File
@@ -1,89 +0,0 @@
#include "app_base.h"
#include "imgui.h"
#include "implot.h"
#include "viz/line_plot.h"
#include "viz/scatter_plot.h"
#include "viz/bar_chart.h"
#include "viz/heatmap.h"
#include "core/app_menubar.h"
#include "core/logger.h"
#include <cmath>
#include <vector>
// Generate sample data
static constexpr int N = 500;
static float xs[N], ys_sin[N], ys_cos[N];
static float scatter_x[200], scatter_y[200];
static const char* bar_labels[] = {"Go", "Python", "Bash", "TypeScript", "C++"};
static float bar_values[] = {201.0f, 202.0f, 38.0f, 80.0f, 5.0f};
static float heat_data[10 * 10];
static bool data_initialized = false;
static void init_data() {
if (data_initialized) return;
fn_log::log_info("init_data: generando %d puntos sin/cos, 200 scatter, 10x10 heatmap", N);
for (int i = 0; i < N; i++) {
xs[i] = static_cast<float>(i) * 0.02f;
ys_sin[i] = sinf(xs[i]);
ys_cos[i] = cosf(xs[i]);
}
for (int i = 0; i < 200; i++) {
scatter_x[i] = static_cast<float>(rand()) / RAND_MAX * 10.0f;
scatter_y[i] = scatter_x[i] * 0.5f + (static_cast<float>(rand()) / RAND_MAX - 0.5f) * 3.0f;
}
for (int i = 0; i < 100; i++) {
int r = i / 10, c = i % 10;
heat_data[i] = sinf(r * 0.5f) * cosf(c * 0.5f);
}
data_initialized = true;
fn_log::log_debug("init_data: ok");
}
void render() {
init_data();
if (ImGui::Begin("fn_registry — Chart Demo")) {
if (ImGui::BeginTabBar("##charts")) {
if (ImGui::BeginTabItem("Line Plot")) {
ImGui::Text("sin(x) — %d points", N);
line_plot("Sine Wave", xs, ys_sin, N);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Scatter Plot")) {
ImGui::Text("y = 0.5x + noise — 200 points");
scatter_plot("Scatter Data", scatter_x, scatter_y, 200);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Bar Chart")) {
ImGui::Text("Functions per language in fn_registry");
bar_chart("Registry Languages", bar_labels, bar_values, 5);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Heatmap")) {
ImGui::Text("sin(r) * cos(c) — 10x10 matrix");
heatmap("Correlation Matrix", heat_data, 10, 10, -1.0f, 1.0f);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
ImGui::End();
}
#ifndef FN_TEST_BUILD
int main() {
return fn::run_app({
.title = "fn_registry — Chart Demo",
.width = 1400,
.height = 900,
.about = {.name = "chart demo",
.version = "0.2.0",
.description = "Demo de primitivos viz: line, scatter, bar, heatmap. AppConfig estandar + multi-viewport."},
.log = {.file_path = "chart_demo.log",
.level = static_cast<int>(fn_log::Level::Debug)}
}, render);
}
#endif
@@ -1,41 +0,0 @@
// E2E tests for chart_demo — Dear ImGui Test Engine.
// Built only when -DFN_BUILD_TESTS=ON. The same main.cpp from chart_demo is
// compiled here with FN_TEST_BUILD defined so its int main() is excluded and
// only render() is reused.
#include "app_base.h"
#include "imgui.h"
#include "imgui_te_engine.h"
#include "imgui_te_context.h"
void render(); // defined in chart_demo/main.cpp
static void register_tests(ImGuiTestEngine* e) {
ImGuiTest* t = nullptr;
// Smoke test: the main window appears and is non-empty.
t = IM_REGISTER_TEST(e, "chart_demo", "smoke_window_visible");
t->TestFunc = [](ImGuiTestContext* ctx) {
ctx->SetRef("fn_registry \xe2\x80\x94 Chart Demo"); // em-dash
IM_CHECK(ctx->WindowInfo("").ID != 0);
};
// Cycle through all four tabs. Test engine fails the test if any tab item
// is not found or cannot be activated — that is our implicit assertion.
t = IM_REGISTER_TEST(e, "chart_demo", "tabs_cycle_all");
t->TestFunc = [](ImGuiTestContext* ctx) {
ctx->SetRef("fn_registry \xe2\x80\x94 Chart Demo");
ctx->ItemClick("##charts/Line Plot");
ctx->ItemClick("##charts/Scatter Plot");
ctx->ItemClick("##charts/Bar Chart");
ctx->ItemClick("##charts/Heatmap");
};
}
int main() {
fn::AppConfig cfg{};
cfg.title = "chart_demo_tests";
cfg.width = 1280;
cfg.height = 800;
return fn::run_app_test(cfg, render, register_tests);
}
Submodule cpp/apps/dag_engine_ui deleted from aec22ba594
Submodule cpp/apps/engine_smoke deleted from bed33856e7
-110
View File
@@ -1,110 +0,0 @@
add_imgui_app(primitives_gallery
main.cpp
capture.cpp
demo.cpp
demos_core.cpp
demos_viz.cpp
demos_graph.cpp
demos_graph_styles.cpp
demos_gfx.cpp
demos_3d.cpp
demos_text_editor.cpp
demos_gl_texture.cpp
demos_extras.cpp
demos_mesh.cpp
# animation primitives (issue 0031)
demos_animation.cpp
${CMAKE_SOURCE_DIR}/functions/core/tween_curves.cpp
${CMAKE_SOURCE_DIR}/functions/core/bezier_editor.cpp
${CMAKE_SOURCE_DIR}/functions/core/timeline.cpp
demos_sql.cpp
demos_scientific.cpp
# text_editor + file_watcher (issue 0025) + file_poll_diff pure (issue 0045)
${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_poll_diff.cpp
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit/TextEditor.cpp
# sql_workbench (issue 0032) + sql_parse pure (issue 0045)
${CMAKE_SOURCE_DIR}/functions/core/sql_workbench.cpp
${CMAKE_SOURCE_DIR}/functions/core/sql_parse.cpp
# Core primitives demoed (tokens vive en fn_framework)
${CMAKE_SOURCE_DIR}/functions/core/fullscreen_window.cpp
${CMAKE_SOURCE_DIR}/functions/core/page_header.cpp
${CMAKE_SOURCE_DIR}/functions/core/dashboard_panel.cpp
${CMAKE_SOURCE_DIR}/functions/core/badge.cpp
${CMAKE_SOURCE_DIR}/functions/core/empty_state.cpp
${CMAKE_SOURCE_DIR}/functions/core/button.cpp
${CMAKE_SOURCE_DIR}/functions/core/icon_button.cpp
${CMAKE_SOURCE_DIR}/functions/core/toolbar.cpp
${CMAKE_SOURCE_DIR}/functions/core/modal_dialog.cpp
${CMAKE_SOURCE_DIR}/functions/core/text_input.cpp
${CMAKE_SOURCE_DIR}/functions/core/select.cpp
${CMAKE_SOURCE_DIR}/functions/core/toast.cpp
${CMAKE_SOURCE_DIR}/functions/core/tree_view.cpp
${CMAKE_SOURCE_DIR}/functions/core/process_runner.cpp
${CMAKE_SOURCE_DIR}/functions/core/process_state_machine.cpp
# Viz primitives demoed
${CMAKE_SOURCE_DIR}/functions/viz/kpi_card.cpp
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/pie_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/histogram.cpp
${CMAKE_SOURCE_DIR}/functions/viz/sparkline.cpp
${CMAKE_SOURCE_DIR}/functions/viz/candlestick.cpp
${CMAKE_SOURCE_DIR}/functions/viz/gauge.cpp
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
${CMAKE_SOURCE_DIR}/functions/viz/table_view.cpp
# 3D viz primitives (issue 0028, ImPlot3D)
${CMAKE_SOURCE_DIR}/functions/viz/surface_plot_3d.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_3d.cpp
# Scientific viz (issue 0034)
${CMAKE_SOURCE_DIR}/functions/viz/treemap.cpp
${CMAKE_SOURCE_DIR}/functions/viz/sankey.cpp
${CMAKE_SOURCE_DIR}/functions/viz/chord.cpp
${CMAKE_SOURCE_DIR}/functions/viz/contour.cpp
${CMAKE_SOURCE_DIR}/functions/viz/voronoi.cpp
# Graph stack (instanced GPU + Barnes-Hut + spatial hash)
${CMAKE_SOURCE_DIR}/functions/viz/graph_types.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_renderer.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_icons.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_force_layout.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_force_layout_gpu.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_layouts.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_viewport.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_viewport_selection.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_labels.cpp
${CMAKE_SOURCE_DIR}/functions/viz/graph_labels_select.cpp
${CMAKE_SOURCE_DIR}/functions/core/graph_spatial_hash.cpp
# GL loader (Linux no-op, Windows wglGetProcAddress)
${CMAKE_SOURCE_DIR}/functions/gfx/gl_loader.cpp
# Shader stack (shader_canvas demo)
${CMAKE_SOURCE_DIR}/functions/gfx/gl_shader.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/gl_framebuffer.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/fullscreen_quad.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/shader_canvas.cpp
# gl_texture_load (issue 0026) + stb_image
${CMAKE_SOURCE_DIR}/functions/gfx/gl_texture_load.cpp
${CMAKE_SOURCE_DIR}/vendor/stb/stb_image_impl.cpp
# mesh_viewer stack (issue 0029)
${CMAKE_SOURCE_DIR}/functions/gfx/mesh_obj_load.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/mesh_gpu.cpp
${CMAKE_SOURCE_DIR}/functions/core/orbit_camera.cpp
${CMAKE_SOURCE_DIR}/functions/viz/mesh_viewer.cpp
)
target_include_directories(primitives_gallery PRIVATE
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit
${CMAKE_SOURCE_DIR}/vendor/stb
)
# SQLite (sql_workbench) — alias provisto por cpp/CMakeLists.txt:
# system on Linux, vendored amalgamation on Windows cross-compile.
target_link_libraries(primitives_gallery PRIVATE SQLite::SQLite3)
if(WIN32)
target_link_libraries(primitives_gallery PRIVATE opengl32)
endif()
if(WIN32)
set_target_properties(primitives_gallery PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
-159
View File
@@ -1,159 +0,0 @@
# primitives_gallery
Catalogo visual interactivo de los primitivos UI del registry (`cpp/functions/core` y `cpp/functions/viz`). Un solo ejecutable con sidebar izquierdo + panel derecho que renderiza la demo del primitivo seleccionado con todas sus variantes y un snippet de codigo.
## Rol
| Funcion | Como lo cumple |
|---|---|
| Smoke test visual | Abrir la gallery tras un cambio en tokens / componentes; si algo se ve raro, lo cazas en segundos. |
| Documentacion viva | Cada demo muestra el componente trabajando + el snippet exacto. Mas rapido que leer los `.md`. |
| Build gate | Esta en el CMake principal (`cpp/CMakeLists.txt`). Si un primitivo rompe API, la gallery no compila => CI rojo. |
| Sandbox de prototipos | Datos sinteticos, sin backend; ideal para iterar un primitivo nuevo sin tocar el dashboard. |
## Build & run
```bash
# Linux
cmake --build cpp/build/linux --target primitives_gallery -j$(nproc)
./cpp/build/linux/apps/primitives_gallery/primitives_gallery
# Windows (cross-compile)
cmake --build cpp/build/windows --target primitives_gallery -j$(nproc)
# binario: cpp/build/windows/apps/primitives_gallery/primitives_gallery.exe
```
No se conecta a `sqlite_api` ni a ningun backend. Datos sinteticos generados in-memory.
## Demos disponibles
### Core
| Demo | Primitivo | Que muestra |
|---|---|---|
| button | `button_cpp_core` | 4 variantes x 3 sizes |
| icon_button | `icon_button_cpp_core` | Glyphs comunes con tooltip |
| toolbar | `toolbar_cpp_core` | Dos grupos con separador vertical |
| modal_dialog | `modal_dialog_cpp_core` | Boton que abre modal con form |
| text_input | `text_input_cpp_core` | 3 inputs con placeholder |
| select | `select_cpp_core` | Dropdown con y sin `(none)` |
| toast + inbox | `toast_cpp_core` (v1.1) | 4 botones que disparan toasts + campana con badge |
| tree_view | `tree_view_cpp_core` | Arbol fake de proyectos -> apps |
| badge | `badge_cpp_core` | 6 variantes semanticas |
| empty_state | `empty_state_cpp_core` | Lista vacia con icono + cta |
| page_header | `page_header_cpp_core` | Header con toolbar a la derecha |
| dashboard_panel | `dashboard_panel_cpp_core` | Panel con titulo y borde |
| kpi_card | `kpi_card_cpp_viz` (v1.2) | Grid 1x4 con sparklines y delta |
### Viz
| Demo | Primitivo | Que muestra |
|---|---|---|
| bar_chart | `bar_chart_cpp_viz` (v1.2) | Labels que caben + labels rotados 45 |
| pie_chart | `pie_chart_cpp_viz` (v1.1) | Pie + donut con tooltip por slice |
| line_plot | `line_plot_cpp_viz` (v1.1) | Serie sintetica `sin(t) + ruido` |
| scatter_plot | `scatter_plot_cpp_viz` (v1.1) | 120 puntos con correlacion |
| histogram | `histogram_cpp_viz` (v1.1) | 300 muestras gaussianas |
| sparkline | `sparkline_cpp_viz` | Trending up / down / flat |
| graph_viewport | `graph_viewport_cpp_viz` | **Ver seccion abajo** |
## Demo `graph_viewport` (en detalle)
Pipeline completo de visualizacion de grafos con instanced GPU rendering:
- `graph_renderer_cpp_viz` (1 draw call para todos los nodos via `glDrawArraysInstanced`)
- `graph_force_layout_cpp_viz` (Barnes-Hut, paso de simulacion por frame)
- `graph_spatial_hash_cpp_core` (hit-testing O(1) bajo el cursor)
- `graph_viewport_cpp_viz` (widget que orquesta los anteriores con pan/zoom/select)
### Controles
| Control | Rango | Efecto |
|---|---|---|
| `Nodes` | 100 20 000 | Numero de nodos a generar |
| `Clusters` | 2 16 | Numero de comunidades (cada una con su color) |
| `Repulsion` | 100 20 000 | Fuerza repulsiva entre todos los nodos. Mas alto => grafo mas extendido y energia mayor. |
| `Attraction` | 0.001 0.5 | Constante del muelle de las aristas. Mas alto => clusters mas compactos. |
| `Gravity` | 0.0 0.05 | Tiron hacia (0,0). Util para evitar drift cuando subes mucho la repulsion. |
| `Regenerate` | boton | Regenera el grafo con los valores actuales de Nodes/Clusters. |
| `Pause / Resume layout` | boton | Para o reanuda la simulacion force-directed. |
| `Fit view` | boton | Encuadra la camara al bounding box del grafo con 10% de padding. |
Los tres sliders de fuerzas se leen cada frame y se inyectan en `ForceLayoutConfig`, asi que cambiar un valor durante el layout en marcha re-calibra el sistema al instante.
### Stats line (sin vibracion)
Una sola linea fija — sin secciones condicionales que cambien la altura del panel:
```
nodes=N edges=E energy=X fps=F | hover=#id cN sel=#id
```
`hover` y `sel` muestran `-` cuando no hay nada seleccionado para mantener el ancho/alto estable; antes una fila condicional desplazaba el viewport en cada hover.
### Interaccion con el viewport
| Gesto | Accion |
|---|---|
| Drag con boton izquierdo en zona vacia | Pan de camara |
| Wheel | Zoom (limites 0.01x 50x) |
| Drag sobre nodo | Mueve el nodo (lo `pin`ea durante el drag) |
| Click sobre nodo | Selecciona (`s_state.selected_node`) |
| Hover sobre nodo | Resaltado + `s_state.hovered_node` poblado |
### Datos sinteticos
`generate_synthetic_graph(N, K)` reparte N nodos en K clusters dispuestos en circulo, con ~3 aristas intra-cluster por nodo y un 5% adicional de aristas inter-cluster. Paleta de 8 colores ABGR. Posiciones iniciales con dispersion gaussiana de 80 px alrededor del centroide del cluster — el force layout las reordena en pocos frames.
### Performance esperada
| Nodes | FPS objetivo (RTX 30xx, viewport 800x460) | Notas |
|---|---|---|
| 1 000 | 60 (vsync) | Caso comun; layout converge < 1 s |
| 5 000 | 60 | Pipeline al limite del CPU para Barnes-Hut |
| 20 000 | 30 50 | El cuello pasa a ser el layout (CPU); GPU render sigue holgado |
Si necesitas mas, fija los nodos (`pinned = true` o `Pause layout`) y veras 60 fps estables — el bottleneck es la simulacion, no el render.
## Anadir un demo nuevo
1. Anadir el prototipo en `demos.h` dentro de `namespace gallery`:
```cpp
void demo_my_thing();
```
2. Implementar el cuerpo en `demos_core.cpp` o `demos_viz.cpp` (o un fichero nuevo si la demo es grande, p.ej. `demos_graph.cpp`).
3. Registrar la entrada en el array `k_demos[]` de `main.cpp`:
```cpp
{"my_thing", "my_thing", "Core" /* o "Viz" */, &gallery::demo_my_thing},
```
4. Si la demo necesita `.cpp` adicionales del registry, anadirlos a `CMakeLists.txt` de la gallery.
5. Recompilar.
## Estructura
```
cpp/apps/primitives_gallery/
CMakeLists.txt # target primitives_gallery
README.md # este fichero
main.cpp # sidebar + router
demo.{h,cpp} # helpers (demo_header, section, code_block, ...)
demos.h # prototipos void demo_xxx()
demos_core.cpp # demos del dominio core
demos_viz.cpp # demos del dominio viz (charts simples)
demos_graph.cpp # demo de graph_viewport (mas pesada, fichero aparte)
```
## Convenciones para los demos
- **Sin estado real**: usar arrays sinteticos (`float fake[] = {...}`) o generadores deterministas con seed fijo. Datos reproducibles.
- **Sin red**: nunca llamar a `sqlite_api`, HTTP, filesystem. La gallery debe arrancar offline en cualquier maquina.
- **Snippets honestos**: el `code_block(...)` debe mostrar el codigo que produce esa demo, no pseudocodigo.
- **Variantes en grids**: si un primitivo tiene N variantes x M tamanos, mostrarlos todos en un `BeginTable` para comparacion lado-a-lado.
- **Estado static**: si la demo es interactiva (sliders, modal, etc.), guardar el estado en `static` locales — la gallery no destruye demos al cambiar de seccion, asi que el estado persiste hasta cerrar la app.
## Iconos en los demos
A partir de la sesion 2026-04-25 los demos usan los macros `TI_*` de `cpp/functions/core/icons_tabler.h` (Tabler v3.41.1, 5093 glyphs). La fuente la carga automaticamente `fn::run_app` via `icon_font_cpp_core`, y `add_imgui_app` copia `tabler-icons.ttf` junto al ejecutable post-build (no hay paso manual).
`demo_icon_button` y `demo_toolbar` (en `demos_core.cpp`) son la referencia visual: muestran el patron `button(TI_PLUS " New", V::Primary)` y la fila de iconos sueltos. Ver `cpp/DESIGN_SYSTEM.md` seccion 11 para la regla.
Si añades un demo nuevo y necesitas glyphs, **no metas `\x..` UTF-8 inline** — busca el icono en `icons_tabler.h` (o en https://tabler.io/icons) y usa el `TI_*` correspondiente.
-37
View File
@@ -1,37 +0,0 @@
---
name: primitives_gallery
lang: cpp
domain: gfx
description: "Visual catalog de primitivas C++ UI del fn_registry. Demos por categoria (charts, controls, layout, gl_info). Soporta modo --capture para regresion visual."
tags: [imgui, gallery, gfx, demo, capture]
uses_functions: []
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/primitives_gallery"
repo_url: ""
---
# primitives_gallery
Catalogo visual de las primitivas y componentes ImGui del registry. Cada demo se carga al hacer click en su entrada del sidebar.
## Build & run
```bash
cd cpp && cmake --build build --target primitives_gallery -j
./build/primitives_gallery
```
## Modo capture (regresion visual)
```bash
./build/primitives_gallery --capture <out_dir>
```
Renderiza cada demo offscreen y guarda PNGs en `<out_dir>/`. Permite gate visual via golden images.
## Notas
- `auto_dockspace = false` — usa `fullscreen_window` que ocupa todo el viewport.
- `init_gl_loader = true` — necesario para demos de OpenGL 4.3 core (compute, SSBOs).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 966 B

-173
View File
@@ -1,173 +0,0 @@
// Implementacion de gallery::run_capture — render offscreen + glReadPixels +
// PNG via stb_image_write. Ver capture.h.
#include "capture.h"
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include "implot.h"
#include "implot3d.h"
#include "core/tokens.h"
#include "core/icon_font.h"
#include "core/app_settings.h"
#include "gfx/gl_loader.h"
#include <GLFW/glfw3.h>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#include <cstdio>
#include <vector>
namespace gallery {
static void glfw_capture_error(int error, const char* description) {
std::fprintf(stderr, "GLFW Error %d: %s\n", error, description);
}
// Flip vertical in-place: OpenGL origin = bottom-left, PNG = top-left.
static void flip_vertical_rgba(unsigned char* px, int w, int h) {
const int stride = w * 4;
std::vector<unsigned char> row(stride);
for (int y = 0; y < h / 2; ++y) {
unsigned char* a = px + y * stride;
unsigned char* b = px + (h - 1 - y) * stride;
std::copy(a, a + stride, row.begin());
std::copy(b, b + stride, a);
std::copy(row.begin(), row.end(), b);
}
}
bool run_capture(const CaptureConfig& cfg, const std::vector<CaptureItem>& items) {
glfwSetErrorCallback(&glfw_capture_error);
if (!glfwInit()) {
std::fprintf(stderr, "capture: glfwInit failed\n");
return false;
}
// Capture mode usa GL 3.3 deliberadamente: WSL Mesa no entrega contexto
// 4.3 offscreen (GLXBadFBConfig). Las pruebas visuales no necesitan
// compute/SSBO — ImGui+ImPlot funciona en 3.3 core. La build interactiva
// (app_base.cpp) si pide 4.3.
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
GLFWwindow* window = glfwCreateWindow(
cfg.capture_w, cfg.capture_h, "capture", nullptr, nullptr);
if (!window) {
std::fprintf(stderr, "capture: glfwCreateWindow failed (no GL?)\n");
glfwTerminate();
return false;
}
glfwMakeContextCurrent(window);
glfwSwapInterval(0);
if (!fn::gfx::gl_loader_init()) {
std::fprintf(stderr, "capture: gl_loader_init failed\n");
glfwDestroyWindow(window);
glfwTerminate();
return false;
}
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImPlot::CreateContext();
ImPlot3D::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.IniFilename = nullptr; // no .ini side effects in capture mode.
io.DisplaySize = ImVec2((float)cfg.capture_w, (float)cfg.capture_h);
fn_ui::settings_load();
fn_ui::load_fonts_from_settings();
{
ImGuiStyle& style = ImGui::GetStyle();
style.FontSizeBase = fn_ui::settings().font_size_px;
style._NextFrameFontSizeBase = style.FontSizeBase;
}
fn_tokens::apply_dark_theme();
ImGui_ImplGlfw_InitForOpenGL(window, false);
ImGui_ImplOpenGL3_Init("#version 330");
bool ok_all = true;
std::vector<unsigned char> pixels((size_t)cfg.capture_w * cfg.capture_h * 4u);
for (const auto& item : items) {
// Warmup: rinde varios frames para que ImGui/ImPlot estabilicen layout
// (el primer frame frecuentemente carece de mediciones de tamaño).
for (int frame = 0; frame < cfg.warmup_frames + 1; ++frame) {
glfwPollEvents();
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// Ventana fullscreen sobre el viewport con la demo activa,
// sin sidebar (queremos el render del primitivo lo mas limpio
// posible para el diff visual).
const ImGuiViewport* vp = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(vp->WorkPos);
ImGui::SetNextWindowSize(vp->WorkSize);
ImGui::Begin("##capture_root",
nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoSavedSettings);
if (item.fn) item.fn();
ImGui::End();
ImGui::Render();
int dw, dh;
glfwGetFramebufferSize(window, &dw, &dh);
glViewport(0, 0, dw, dh);
glClearColor(fn_tokens::colors::bg.x,
fn_tokens::colors::bg.y,
fn_tokens::colors::bg.z, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
}
// Read framebuffer (GL_RGBA / GL_UNSIGNED_BYTE).
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, cfg.capture_w, cfg.capture_h,
GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
flip_vertical_rgba(pixels.data(), cfg.capture_w, cfg.capture_h);
char path[1024];
std::snprintf(path, sizeof(path), "%s/%s.png",
cfg.output_dir.c_str(), item.id.c_str());
const int rc = stbi_write_png(
path, cfg.capture_w, cfg.capture_h, 4,
pixels.data(), cfg.capture_w * 4);
if (rc == 0) {
std::fprintf(stderr, "capture: stbi_write_png failed for %s\n", path);
ok_all = false;
} else {
std::fprintf(stdout, "captured: %s\n", path);
}
}
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImPlot3D::DestroyContext();
ImPlot::DestroyContext();
ImGui::DestroyContext();
glfwDestroyWindow(window);
glfwTerminate();
return ok_all;
}
} // namespace gallery
-34
View File
@@ -1,34 +0,0 @@
#pragma once
// Capture mode: renderiza cada demo de la gallery en una ventana GLFW
// invisible y guarda un PNG en `output_dir/<demo_id>.png` via stb_image_write.
//
// Diseñado para CI / golden-image diffing: ver `cpp/scripts/update_goldens.sh`
// y `cpp/tests/test_visual.cpp`.
//
// Importante:
// - Requiere un contexto OpenGL real. En entornos sin GPU (containers minimos)
// funciona con `LIBGL_ALWAYS_SOFTWARE=1` (Mesa/llvmpipe) o swiftshader.
// - Si el entorno (WSL sin GL) no puede crear un contexto GL valido, el
// binario sale con codigo != 0 sin generar PNGs.
#include <string>
#include <vector>
namespace gallery {
struct CaptureItem {
std::string id;
void (*fn)();
};
struct CaptureConfig {
std::string output_dir;
int warmup_frames = 3;
int capture_w = 800;
int capture_h = 600;
};
// Devuelve true si todo el set se capturo OK.
bool run_capture(const CaptureConfig& cfg, const std::vector<CaptureItem>& items);
} // namespace gallery
-76
View File
@@ -1,76 +0,0 @@
#include "demo.h"
#include "core/tokens.h"
#include <cstdio>
namespace gallery {
void demo_header(const char* name, const char* version, const char* description) {
using namespace fn_tokens;
ImGui::SetWindowFontScale(1.4f);
ImGui::TextUnformatted(name);
ImGui::SetWindowFontScale(1.0f);
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::Text(" %s", version);
ImGui::PopStyleColor();
if (description && *description) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextWrapped("%s", description);
ImGui::PopStyleColor();
}
ImGui::Separator();
ImGui::Dummy(ImVec2(0, spacing::sm));
}
void section(const char* title) {
using namespace fn_tokens;
ImGui::Dummy(ImVec2(0, spacing::sm));
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextUnformatted(title);
ImGui::PopStyleColor();
ImGui::Separator();
ImGui::Dummy(ImVec2(0, spacing::xs));
}
void variant_label(const char* text) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim);
ImGui::TextUnformatted(text);
ImGui::PopStyleColor();
}
void code_block(const char* code) {
using namespace fn_tokens;
ImGui::Dummy(ImVec2(0, spacing::sm));
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::TextUnformatted("// example");
ImGui::PopStyleColor();
ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::bg);
ImGui::PushStyleColor(ImGuiCol_Border, colors::border);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, radius::sm);
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::md, spacing::sm));
// Altura: aprox lineas * line-height
int lines = 1;
for (const char* p = code; *p; ++p) if (*p == '\n') ++lines;
float h = lines * ImGui::GetTextLineHeightWithSpacing() + spacing::md;
char id[32];
std::snprintf(id, sizeof(id), "##code_%p", (const void*)code);
ImGui::BeginChild(id, ImVec2(0, h),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PushStyleColor(ImGuiCol_Text, colors::text);
ImGui::TextUnformatted(code);
ImGui::PopStyleColor();
ImGui::EndChild();
ImGui::PopStyleVar(3);
ImGui::PopStyleColor(2);
}
} // namespace gallery
-22
View File
@@ -1,22 +0,0 @@
#pragma once
// Helpers compartidos por todas las demos de la gallery.
// No son primitivos del registry — son utilidades locales de este app.
#include "imgui.h"
#include <string>
namespace gallery {
// Titulo + version + descripcion en la parte superior del panel derecho.
void demo_header(const char* name, const char* version, const char* description);
// Seccion secundaria dentro de una demo (agrupar variantes).
void section(const char* title);
// Bloque de codigo monoespaciado con bg surface y label "// example".
void code_block(const char* code);
// Etiqueta sutil encima de un grupo de widgets.
void variant_label(const char* text);
} // namespace gallery
-56
View File
@@ -1,56 +0,0 @@
#pragma once
// Cada demo_xxx() renderiza una seccion completa para un primitivo.
// Se llaman desde main.cpp en funcion del item seleccionado en el sidebar.
namespace gallery {
// --- Core ---
void demo_button();
void demo_icon_button();
void demo_toolbar();
void demo_modal();
void demo_text_input();
void demo_select();
void demo_toast();
void demo_tree_view();
void demo_kpi_card();
void demo_badge();
void demo_empty_state();
void demo_page_header();
void demo_dashboard_panel();
void demo_text_editor(); // wave 1, issue 0025
void demo_file_watcher(); // wave 1, issue 0025
void demo_process_runner();
void demo_tween(); // issue 0031
void demo_bezier_editor(); // issue 0031
void demo_timeline(); // issue 0031
void demo_sql_workbench(); // issue 0032
// --- Viz ---
void demo_bar_chart();
void demo_pie_chart();
void demo_line_plot();
void demo_scatter_plot();
void demo_histogram();
void demo_sparkline();
void demo_graph();
void demo_graph_styles(); // issue 0049f
void demo_candlestick();
void demo_gauge();
void demo_heatmap();
void demo_table_view();
void demo_surface_plot_3d(); // issue 0028, ImPlot3D
void demo_scatter_3d(); // issue 0028, ImPlot3D
void demo_mesh_viewer(); // issue 0029
void demo_treemap(); // issue 0034
void demo_sankey(); // issue 0034
void demo_chord(); // issue 0034
void demo_contour(); // issue 0034
void demo_voronoi(); // issue 0034
// --- Gfx ---
void demo_shader_canvas();
void demo_gl_texture(); // wave 1, issue 0026
void demo_gl_info(); // issue 0049b — runtime GL version + 4.3 caps
} // namespace gallery
-100
View File
@@ -1,100 +0,0 @@
// demos_3d — demos para los primitivos viz/* basados en ImPlot3D.
// Issue 0028: surface_plot_3d real + scatter_3d.
#include "demos.h"
#include "demo.h"
#include "viz/surface_plot_3d.h"
#include "viz/scatter_3d.h"
#include <imgui.h>
#include <cmath>
#include <random>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// surface_plot_3d
// ---------------------------------------------------------------------------
void demo_surface_plot_3d() {
demo_header("surface_plot_3d", "v2.0.0",
"Superficie 3D ImPlot3D (z = A * sin(fx*x) * cos(fy*y)) con sliders para "
"ajustar las frecuencias en tiempo real. Drag para orbitar, wheel para zoom.");
section("Malla 64x64 — sin(fx*x) * cos(fy*y)");
static float fx = 0.20f;
static float fy = 0.20f;
static float amp = 1.0f;
ImGui::SliderFloat("fx", &fx, 0.05f, 1.0f, "%.2f");
ImGui::SliderFloat("fy", &fy, 0.05f, 1.0f, "%.2f");
ImGui::SliderFloat("amplitud", &amp, 0.1f, 3.0f, "%.2f");
constexpr int N = 64;
static std::vector<float> z(N * N);
for (int j = 0; j < N; ++j) {
for (int i = 0; i < N; ++i) {
z[j * N + i] = amp * std::sin(fx * float(i)) * std::cos(fy * float(j));
}
}
fn::SurfacePlot3DConfig cfg{};
cfg.z = z.data();
cfg.nx = N; cfg.ny = N;
cfg.x_min = 0.f; cfg.x_max = float(N);
cfg.y_min = 0.f; cfg.y_max = float(N);
cfg.size = ImVec2(-1.f, 420.f);
fn::surface_plot_3d("##gallery_surface", cfg);
}
// ---------------------------------------------------------------------------
// scatter_3d
// ---------------------------------------------------------------------------
void demo_scatter_3d() {
demo_header("scatter_3d", "v1.0.0",
"Scatter 3D ImPlot3D con color por punto. 3 clusters gaussianos sinteticos "
"(N=500) para simular una visualizacion tipica de PCA / clustering.");
section("3 clusters gaussianos (500 puntos)");
constexpr int N = 500;
static std::vector<float> xs(N), ys(N), zs(N);
static std::vector<ImU32> colors(N);
static bool initialized = false;
if (!initialized) {
std::mt19937 rng(42);
std::normal_distribution<float> g(0.f, 0.4f);
const ImU32 palette[3] = {
IM_COL32(255, 99, 71, 255), // tomate
IM_COL32( 65, 170, 255, 255), // azul
IM_COL32(120, 220, 120, 255), // verde
};
const float cx[3] = {-1.5f, 1.5f, 0.f};
const float cy[3] = { 0.f, 0.f, 2.0f};
const float cz[3] = { 0.f, 1.0f,-1.0f};
for (int i = 0; i < N; ++i) {
int c = i % 3;
xs[i] = cx[c] + g(rng);
ys[i] = cy[c] + g(rng);
zs[i] = cz[c] + g(rng);
colors[i] = palette[c];
}
initialized = true;
}
fn::Scatter3DConfig cfg{};
cfg.xs = xs.data();
cfg.ys = ys.data();
cfg.zs = zs.data();
cfg.colors = colors.data();
cfg.n = N;
cfg.size = ImVec2(-1.f, 420.f);
fn::scatter_3d("##gallery_clusters", cfg);
}
} // namespace gallery
@@ -1,249 +0,0 @@
// Demos para los primitivos de animacion (issue 0031):
// - tween_curves
// - bezier_editor
// - timeline
#include "demos.h"
#include "demo.h"
#include "core/tween_curves.h"
#include "core/bezier_editor.h"
#include "core/timeline.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cstdio>
#include <cmath>
namespace gallery {
// ---------------------------------------------------------------------------
// demo_tween — dropdown + plot animado
// ---------------------------------------------------------------------------
void demo_tween() {
using namespace fn_tokens;
using fn::tween::Ease;
demo_header("tween_curves", "v1.0.0",
"Funciones de easing (Penner): linear, quad, cubic, expo, elastic, "
"bounce con variantes in/out/inOut. Header-mostly: el compilador "
"inlinea cada curva en el sitio de llamada.");
section("Selector + plot");
static int ease_idx = (int)Ease::OutCubic;
static float anim_t = 0.0f;
anim_t += ImGui::GetIO().DeltaTime * 0.5f;
if (anim_t > 1.5f) anim_t = -0.25f; // hold un poco antes de reiniciar
// Build labels
const char* labels[fn::tween::ease_count];
for (int i = 0; i < fn::tween::ease_count; i++) {
labels[i] = fn::tween::name((Ease)i);
}
ImGui::SetNextItemWidth(220.0f);
ImGui::Combo("##tween_ease", &ease_idx, labels, fn::tween::ease_count);
Ease ease = (Ease)ease_idx;
float t_clamped = anim_t;
if (t_clamped < 0.0f) t_clamped = 0.0f;
if (t_clamped > 1.0f) t_clamped = 1.0f;
float v = fn::tween::apply(ease, t_clamped);
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::Text(" t=%.2f f(t)=%.3f", t_clamped, v);
ImGui::PopStyleColor();
// Canvas plot
ImVec2 canvas_min = ImGui::GetCursorScreenPos();
ImVec2 canvas_size(360.0f, 220.0f);
ImVec2 canvas_max = ImVec2(canvas_min.x + canvas_size.x, canvas_min.y + canvas_size.y);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(canvas_min, canvas_max, ImGui::GetColorU32(colors::bg), radius::sm);
dl->AddRect(canvas_min, canvas_max, ImGui::GetColorU32(colors::border), radius::sm);
auto to_px = [&](float tx, float ty) {
// ty puede salir de [0,1] (elastic/bounce); damos algo de margen vertical.
return ImVec2(canvas_min.x + tx * canvas_size.x,
canvas_min.y + (1.0f - ty) * canvas_size.y);
};
// Grid 4x4
ImU32 grid = ImGui::GetColorU32(colors::border);
for (int i = 1; i < 4; i++) {
float fx = canvas_min.x + canvas_size.x * (float)i / 4.0f;
float fy = canvas_min.y + canvas_size.y * (float)i / 4.0f;
dl->AddLine(ImVec2(fx, canvas_min.y), ImVec2(fx, canvas_max.y), grid);
dl->AddLine(ImVec2(canvas_min.x, fy), ImVec2(canvas_max.x, fy), grid);
}
// Diagonal linear
dl->AddLine(to_px(0.0f, 0.0f), to_px(1.0f, 1.0f),
ImGui::GetColorU32(colors::text_dim), 1.0f);
// Curva
constexpr int N = 96;
ImVec2 prev = to_px(0.0f, fn::tween::apply(ease, 0.0f));
ImU32 col = ImGui::GetColorU32(colors::primary);
for (int i = 1; i <= N; i++) {
float x = (float)i / (float)N;
float y = fn::tween::apply(ease, x);
ImVec2 cur = to_px(x, y);
dl->AddLine(prev, cur, col, 2.0f);
prev = cur;
}
// Marker animado
ImVec2 m = to_px(t_clamped, v);
dl->AddCircleFilled(m, 5.0f, ImGui::GetColorU32(colors::primary_light));
dl->AddCircle(m, 6.0f, ImGui::GetColorU32(colors::text), 0, 1.5f);
// Avanzar cursor
ImGui::Dummy(canvas_size);
code_block(
"#include \"core/tween_curves.h\"\n\n"
"float k = fn::tween::apply(fn::tween::Ease::OutCubic, t);\n"
"// o named:\n"
"float k2 = fn::tween::out_cubic(t);"
);
}
// ---------------------------------------------------------------------------
// demo_bezier_editor — editor + plot evaluado
// ---------------------------------------------------------------------------
void demo_bezier_editor() {
using namespace fn_tokens;
demo_header("bezier_editor", "v1.0.0",
"Editor visual de curva Bezier cubica (4 puntos). Para diseñar "
"easing curves custom. p1/p2 son draggable; p0/p3 fijos en (0,0)/(1,1).");
section("Editor");
static fn::BezierCurve curve; // identidad por defecto: ease lineal con handles desplazados
if (ImGui::Button("Reset##bz_reset")) {
curve = fn::BezierCurve{};
}
ImGui::SameLine();
if (ImGui::Button("Ease-out preset##bz_eo")) {
curve = {{0,0}, {0.0f, 0.0f}, {0.58f, 1.0f}, {1,1}};
}
ImGui::SameLine();
if (ImGui::Button("Ease-in-out preset##bz_eio")) {
curve = {{0,0}, {0.42f, 0.0f}, {0.58f, 1.0f}, {1,1}};
}
fn::bezier_editor("##bz_editor", curve, ImVec2(220, 220));
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::Text("p0=(%.2f,%.2f) p1=(%.2f,%.2f) p2=(%.2f,%.2f) p3=(%.2f,%.2f)",
curve.p0.x, curve.p0.y, curve.p1.x, curve.p1.y,
curve.p2.x, curve.p2.y, curve.p3.x, curve.p3.y);
ImGui::PopStyleColor();
// Plot evaluation
section("bezier_eval(curve, t)");
static float t = 0.0f;
ImGui::SetNextItemWidth(360.0f);
ImGui::SliderFloat("t##bz_t", &t, 0.0f, 1.0f, "%.3f");
float y = fn::bezier_eval(curve, t);
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::Text("y(t=%.3f) = %.3f", t, y);
ImGui::PopStyleColor();
code_block(
"#include \"core/bezier_editor.h\"\n\n"
"static fn::BezierCurve curve;\n"
"if (fn::bezier_editor(\"##my\", curve, ImVec2(220, 220))) {\n"
" // user dragged a control point\n"
"}\n"
"float k = fn::bezier_eval(curve, t);"
);
}
// ---------------------------------------------------------------------------
// demo_timeline — 2 tracks + display
// ---------------------------------------------------------------------------
void demo_timeline() {
using namespace fn_tokens;
using fn::tween::Ease;
demo_header("timeline", "v1.0.0",
"Timeline tipo DAW: tracks horizontales con keyframes draggable, "
"scrub con el ruler, play/pause/loop. track_value_at(t) interpola "
"aplicando la Ease de cada keyframe destino.");
static fn::TimelineState tl;
static bool inited = false;
if (!inited) {
tl.duration = 4.0f;
tl.playing = true;
tl.tracks.push_back({"hue", {
{0.0f, 0.0f, Ease::Linear},
{2.0f, 1.0f, Ease::OutCubic},
{4.0f, 0.0f, Ease::InOutCubic},
}});
tl.tracks.push_back({"amp", {
{0.0f, 0.2f, Ease::Linear},
{3.0f, 1.0f, Ease::OutElastic},
}});
inited = true;
}
// Update
fn::timeline_update(tl, ImGui::GetIO().DeltaTime);
// Display values
section("Live values");
float hue = fn::track_value_at(tl.tracks[0], tl.current_time);
float amp = fn::track_value_at(tl.tracks[1], tl.current_time);
ImGui::PushStyleColor(ImGuiCol_Text, colors::text);
ImGui::Text("t = %.3fs", tl.current_time);
ImGui::PopStyleColor();
auto draw_bar = [&](const char* name, float value, float vmin, float vmax) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::Text("%-4s", name);
ImGui::PopStyleColor();
ImGui::SameLine();
ImVec2 cmin = ImGui::GetCursorScreenPos();
ImVec2 csize = ImVec2(280.0f, 14.0f);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(cmin, ImVec2(cmin.x + csize.x, cmin.y + csize.y),
ImGui::GetColorU32(colors::surface_active), radius::sm);
float k = (value - vmin) / (vmax - vmin);
if (k < 0.0f) k = 0.0f;
if (k > 1.0f) k = 1.0f;
dl->AddRectFilled(cmin, ImVec2(cmin.x + csize.x * k, cmin.y + csize.y),
ImGui::GetColorU32(colors::primary), radius::sm);
ImGui::Dummy(csize);
ImGui::SameLine();
ImGui::Text("%.3f", value);
};
draw_bar("hue", hue, 0.0f, 1.0f);
draw_bar("amp", amp, 0.0f, 1.0f);
section("Widget");
fn::timeline_widget("##gallery_tl", tl, ImVec2(-1, 220));
code_block(
"#include \"core/timeline.h\"\n\n"
"static fn::TimelineState tl;\n"
"tl.tracks.push_back({\"hue\", {{0,0}, {2,1, fn::tween::Ease::OutCubic}, {4,0}}});\n"
"tl.duration = 4.0f; tl.playing = true;\n\n"
"fn::timeline_update(tl, ImGui::GetIO().DeltaTime);\n"
"float h = fn::track_value_at(tl.tracks[0], tl.current_time);\n"
"fn::timeline_widget(\"##tl\", tl);"
);
}
} // namespace gallery
-447
View File
@@ -1,447 +0,0 @@
#include "demos.h"
#include "demo.h"
#include "core/button.h"
#include "core/icon_button.h"
#include "core/toolbar.h"
#include "core/modal_dialog.h"
#include "core/text_input.h"
#include "core/select.h"
#include "core/toast.h"
#include "core/tree_view.h"
#include "core/badge.h"
#include "core/empty_state.h"
#include "core/page_header.h"
#include "core/dashboard_panel.h"
#include "core/tokens.h"
#include "core/icons_tabler.h"
#include "viz/kpi_card.h"
#include <imgui.h>
#include <cstdio>
using namespace fn_ui;
using V = ButtonVariant;
using S = ButtonSize;
namespace gallery {
// ---------------------------------------------------------------------------
// button
// ---------------------------------------------------------------------------
void demo_button() {
demo_header("button", "v1.0.0",
"Boton con 4 variantes semanticas y 3 tamanos. Usa tokens para colores, "
"radius y padding — estilo consistente en toda la app.");
section("Variants x Sizes");
const V variants[] = {V::Primary, V::Secondary, V::Subtle, V::Danger};
const char* variant_names[] = {"Primary", "Secondary", "Subtle", "Danger"};
const S sizes[] = {S::Sm, S::Md, S::Lg};
const char* size_names[] = {"sm", "md", "lg"};
if (ImGui::BeginTable("##btn_grid", 5, ImGuiTableFlags_SizingFixedFit)) {
ImGui::TableSetupColumn("size");
for (int c = 0; c < 4; c++) ImGui::TableSetupColumn(variant_names[c]);
ImGui::TableHeadersRow();
for (int s = 0; s < 3; s++) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
variant_label(size_names[s]);
for (int v = 0; v < 4; v++) {
ImGui::TableSetColumnIndex(v + 1);
char id[32];
std::snprintf(id, sizeof(id), "%s##%d%d", variant_names[v], s, v);
button(id, variants[v], sizes[s]);
}
}
ImGui::EndTable();
}
code_block(
"#include \"core/button.h\"\n"
"using fn_ui::button;\n"
"using V = fn_ui::ButtonVariant;\n\n"
"if (button(\"Save\", V::Primary)) save();\n"
"if (button(\"Cancel\", V::Subtle)) close();\n"
"if (button(\"Delete\", V::Danger)) confirm();"
);
}
// ---------------------------------------------------------------------------
// icon_button
// ---------------------------------------------------------------------------
void demo_icon_button() {
demo_header("icon_button", "v1.0.0",
"Boton cuadrado 28x28 con un glyph centrado y tooltip opcional. "
"Usa los TI_* de core/icons_tabler.h (Tabler Icons cargado automaticamente "
"por fn::run_app via icon_font.cpp).");
section("Tabler icon set");
struct { const char* id; const char* glyph; const char* tip; } ic[] = {
{"##rl", TI_REFRESH, "Reload"},
{"##ad", TI_PLUS, "Add"},
{"##dl", TI_TRASH, "Delete"},
{"##dn", TI_CHEVRON_DOWN, "Dropdown"},
{"##cf", TI_SETTINGS, "Settings"},
{"##ok", TI_CHECK, "Check"},
{"##cl", TI_X, "Close"},
{"##ed", TI_PENCIL, "Edit"},
{"##sv", TI_DEVICE_FLOPPY, "Save"},
{"##sr", TI_SEARCH, "Search"},
{"##hp", TI_HELP, "Help"},
{"##hm", TI_HOME, "Home"},
};
for (auto& b : ic) {
icon_button(b.id, b.glyph, b.tip);
ImGui::SameLine();
}
ImGui::NewLine();
code_block(
"#include \"core/icons_tabler.h\"\n\n"
"if (icon_button(\"##reload\", TI_REFRESH, \"Reload\"))\n"
" reload_data();\n\n"
"// Mas de 5000 iconos disponibles — ver core/icons_tabler.h"
);
}
// ---------------------------------------------------------------------------
// toolbar
// ---------------------------------------------------------------------------
void demo_toolbar() {
demo_header("toolbar", "v1.0.0",
"Grupo horizontal con spacing consistente y separadores verticales sutiles. "
"El caller usa ImGui::SameLine entre items y toolbar_separator entre grupos.");
section("Example with two groups");
toolbar_begin();
button(TI_PLUS " New", V::Primary); ImGui::SameLine();
button(TI_FOLDER_OPEN " Open", V::Secondary); ImGui::SameLine();
button(TI_DEVICE_FLOPPY " Save",V::Secondary);
toolbar_separator();
icon_button("##set", TI_SETTINGS, "Settings");
ImGui::SameLine();
icon_button("##help", TI_HELP, "Help");
toolbar_end();
code_block(
"#include \"core/icons_tabler.h\"\n\n"
"toolbar_begin();\n"
" button(TI_PLUS \" New\", V::Primary); ImGui::SameLine();\n"
" button(TI_FOLDER_OPEN \" Open\", V::Secondary);\n"
" toolbar_separator();\n"
" icon_button(\"##set\", TI_SETTINGS, \"Settings\");\n"
"toolbar_end();"
);
}
// ---------------------------------------------------------------------------
// modal_dialog
// ---------------------------------------------------------------------------
void demo_modal() {
demo_header("modal_dialog", "v1.0.0",
"Popup modal centrada con estilo surface+border. Close con Escape o click en X. "
"Patron begin/end — modal_dialog_end debe llamarse siempre.");
static bool show = false;
if (button("Open modal", V::Primary)) show = true;
if (modal_dialog_begin("Demo modal", &show, ImVec2(380, 0))) {
ImGui::TextWrapped(
"Modal centrada en el viewport principal, con estilo tokens.");
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm));
static char buf[64] = {};
text_input("Name", buf, sizeof(buf), "escribe algo");
ImGui::Separator();
if (button("Cancel", V::Subtle)) show = false;
ImGui::SameLine();
if (button("Done", V::Primary)) show = false;
}
modal_dialog_end();
code_block(
"static bool show = false;\n"
"if (button(\"Open\", Primary)) show = true;\n"
"if (modal_dialog_begin(\"Title\", &show, ImVec2(380,0))) {\n"
" // ... campos del form ...\n"
" if (button(\"Done\", Primary)) show = false;\n"
"}\n"
"modal_dialog_end();"
);
}
// ---------------------------------------------------------------------------
// text_input
// ---------------------------------------------------------------------------
void demo_text_input() {
demo_header("text_input", "v1.0.0",
"Label muted + input estilizado con tokens. Full-width dentro del contenedor. "
"Placeholder opcional mostrado en text_dim cuando el buffer esta vacio.");
static char name[128] = {};
static char desc[256] = {};
static char tags[128] = {};
ImGui::BeginChild("##ti_wrap", ImVec2(420, 0), ImGuiChildFlags_AutoResizeY);
text_input("Name", name, sizeof(name), "my-new-thing");
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
text_input("Description", desc, sizeof(desc));
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
text_input("Tags (CSV)", tags, sizeof(tags), "imgui,ui,form");
ImGui::EndChild();
code_block(
"static char name[128] = {};\n"
"text_input(\"Name\", name, sizeof(name), \"my-new-thing\");\n"
"// true on change — se usa mas para validar en vivo\n"
"// que para leer el valor (que vive en el buffer)."
);
}
// ---------------------------------------------------------------------------
// select
// ---------------------------------------------------------------------------
void demo_select() {
demo_header("select", "v1.0.0",
"Dropdown con label muted y opcion (none) opcional. Mismo estilo tokens que text_input.");
static int lang_idx = 0;
static int domain_idx = -1;
const char* langs[] = {"go", "py", "ts", "sh", "cpp"};
const char* domains[] = {"core", "infra", "finance", "datascience", "viz"};
ImGui::BeginChild("##sl_wrap", ImVec2(420, 0), ImGuiChildFlags_AutoResizeY);
select("Language", &lang_idx, langs, 5);
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs));
select("Domain (optional)", &domain_idx, domains, 5, true);
ImGui::EndChild();
code_block(
"static int lang = 0;\n"
"const char* langs[] = {\"go\",\"py\",\"ts\",\"sh\",\"cpp\"};\n"
"select(\"Language\", &lang, langs, 5);"
);
}
// ---------------------------------------------------------------------------
// toast + inbox
// ---------------------------------------------------------------------------
void demo_toast() {
demo_header("toast", "v1.1.0",
"Notificaciones efimeras (~3.5s con fade-out) + inbox con campana. "
"La campana muestra badge con no-leidos y popover con las ultimas 50.");
section("Trigger toasts");
if (button("Info", V::Secondary)) toast_push(ToastKind::Info, "this is an info toast");
ImGui::SameLine();
if (button("Success", V::Primary)) toast_push(ToastKind::Success, "operation completed");
ImGui::SameLine();
if (button("Warning", V::Secondary)) toast_push(ToastKind::Warning, "heads up about something");
ImGui::SameLine();
if (button("Error", V::Danger)) toast_push(ToastKind::Error, "operation failed: reason");
section("Inbox (bell with unread badge)");
toast_inbox_button("##inbox_demo");
code_block(
"toast_push(ToastKind::Success, \"Reindexed 891 functions\");\n"
"toast_push(ToastKind::Error, \"HTTP 503: server down\");\n\n"
"// En la toolbar:\n"
"toast_inbox_button(\"##inbox\");\n\n"
"// Una vez por frame al final del render:\n"
"toast_render();"
);
}
// ---------------------------------------------------------------------------
// tree_view
// ---------------------------------------------------------------------------
void demo_tree_view() {
demo_header("tree_view", "v1.0.0",
"Tree low-level para jerarquias (ej. projects -> apps/analysis/vaults). "
"Sin estado interno: el caller gestiona seleccion y pasa 'selected' por parametro.");
static std::string selected;
section("Projects (fake)");
ImGui::BeginChild("##tv", ImVec2(360, 200), ImGuiChildFlags_Borders);
struct FakeProject { const char* id; const char* name; const char* apps[3]; };
const FakeProject projs[] = {
{"app_turismo", "app_turismo", {"guide_es", "offline_maps", nullptr}},
{"element_agents", "element_agents", {"matrix_bot", nullptr, nullptr}},
{"fn_monitoring", "fn_monitoring", {"sqlite_api", "registry_dashboard", nullptr}},
};
for (auto& p : projs) {
bool sel = (selected == p.id);
if (tree_branch_begin(p.id, p.name, sel)) {
if (tree_node_clicked()) selected = p.id;
for (int i = 0; i < 3 && p.apps[i]; i++) {
bool asel = (selected == p.apps[i]);
tree_leaf(p.apps[i], p.apps[i], asel);
if (tree_node_clicked()) selected = p.apps[i];
}
tree_branch_end();
} else if (tree_node_clicked()) {
selected = p.id;
}
}
ImGui::EndChild();
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::Text("Selected: %s", selected.empty() ? "(none)" : selected.c_str());
ImGui::PopStyleColor();
code_block(
"static std::string sel;\n"
"if (tree_branch_begin(p.id, p.name, sel == p.id)) {\n"
" if (tree_node_clicked()) sel = p.id;\n"
" for (auto& a : p.apps) {\n"
" tree_leaf(a.id, a.name, sel == a.id);\n"
" if (tree_node_clicked()) sel = a.id;\n"
" }\n"
" tree_branch_end();\n"
"}"
);
}
// ---------------------------------------------------------------------------
// kpi_card
// ---------------------------------------------------------------------------
void demo_kpi_card() {
demo_header("kpi_card", "v1.3.0",
"Card compacta 86px con icono opcional + label muted, valor x1.4, trend con "
"TI_TRENDING_UP/DOWN y sparkline. Usa tokens: surface bg, border, radius md.");
if (ImGui::BeginTable("##kpi_grid", 4, ImGuiTableFlags_SizingStretchSame)) {
float history[] = {10, 12, 11, 15, 18, 17, 20};
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f", TI_CASH);
ImGui::TableSetColumnIndex(1); kpi_card("Users", 1250.0f, 3.4f, history, 7, "%.0f", TI_USERS);
ImGui::TableSetColumnIndex(2); kpi_card("Churn", 2.1f, -0.3f, history, 7, "%.1f%%", TI_CHART_BAR);
ImGui::TableSetColumnIndex(3); kpi_card("Errors", 0.0f, 0.0f, nullptr, 0, "%.0f", TI_ALERT_CIRCLE);
ImGui::EndTable();
}
code_block(
"#include \"core/icons_tabler.h\"\n\n"
"float history[] = {10,12,11,15,18,17,20};\n"
"kpi_card(\"Revenue\", 20000.0f, 12.5f, history, 7, \"$%.0f\", TI_CASH);\n"
"kpi_card(\"Users\", 1250.0f, 3.4f, history, 7, \"%.0f\", TI_USERS);\n"
"// Sin delta ni history: muestra TI_MINUS como placeholder\n"
"kpi_card(\"Errors\", 0.0f, 0.0f, nullptr, 0, \"%.0f\", TI_ALERT_CIRCLE);"
);
}
// ---------------------------------------------------------------------------
// badge
// ---------------------------------------------------------------------------
void demo_badge() {
demo_header("badge", "v1.0.0",
"Etiqueta inline con 6 variantes semanticas. Equivalente a <Badge> de fn_library.");
section("Variants");
badge("Default", BadgeVariant::Default); ImGui::SameLine();
badge("Success", BadgeVariant::Success); ImGui::SameLine();
badge("Warning", BadgeVariant::Warning); ImGui::SameLine();
badge("Error", BadgeVariant::Error); ImGui::SameLine();
badge("Info", BadgeVariant::Info); ImGui::SameLine();
badge("Outline", BadgeVariant::Outline);
section("In context (table row)");
ImGui::Text("filter_slice_go_core"); ImGui::SameLine();
badge("pure", BadgeVariant::Success); ImGui::SameLine();
badge("tested", BadgeVariant::Info);
code_block(
"badge(\"pure\", BadgeVariant::Success);\n"
"badge(\"stale\", BadgeVariant::Warning);\n"
"badge(\"broken\", BadgeVariant::Error);"
);
}
// ---------------------------------------------------------------------------
// empty_state
// ---------------------------------------------------------------------------
void demo_empty_state() {
demo_header("empty_state", "v1.0.0",
"Icono grande muted + titulo + descripcion opcional. Para listas/tablas vacias.");
ImGui::BeginChild("##es", ImVec2(0, 180), ImGuiChildFlags_Borders);
empty_state("( no data )", "No projects yet",
"Create one under projects/{name}/ with project.md and reindex");
ImGui::EndChild();
code_block(
"if (apps.empty()) {\n"
" empty_state(\"( no data )\", \"No apps yet\",\n"
" \"Click + Add to create one\");\n"
" return;\n"
"}"
);
}
// ---------------------------------------------------------------------------
// page_header
// ---------------------------------------------------------------------------
void demo_page_header() {
demo_header("page_header", "v1.0.0",
"Header de pagina con titulo, subtitulo opcional y separador final. "
"Patron begin/end permite insertar acciones entre titulo y separador.");
page_header_begin("Dashboard", "13 apps, 3 projects, 2 analyses");
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 140.0f);
toolbar_begin();
button("Reload", V::Subtle); ImGui::SameLine();
button("+ Add", V::Secondary);
toolbar_end();
page_header_end();
code_block(
"page_header_begin(\"Dashboard\", subtitle);\n"
"ImGui::SameLine(ImGui::GetContentRegionAvail().x - 140);\n"
"toolbar_begin();\n"
" button(\"Reload\", Subtle);\n"
"toolbar_end();\n"
"page_header_end();"
);
}
// ---------------------------------------------------------------------------
// dashboard_panel
// ---------------------------------------------------------------------------
void demo_dashboard_panel() {
demo_header("dashboard_panel", "v1.0.0",
"Contenedor tipo panel con titulo, bordes redondeados, bg surface. "
"Auto-resize-Y segun contenido. Usa min_width/min_height como piso.");
if (dashboard_panel_begin("Revenue", 0, 120.0f)) {
ImGui::Text("Some panel content goes here.");
ImGui::Text("Anything drawn inside lives in the child window.");
}
dashboard_panel_end();
code_block(
"if (dashboard_panel_begin(\"Revenue\", 0, 120.0f)) {\n"
" ImGui::Text(\"content\");\n"
"}\n"
"dashboard_panel_end();"
);
}
} // namespace gallery
@@ -1,215 +0,0 @@
// Demos faltantes: process_runner (Core), candlestick / gauge / heatmap /
// table_view (Viz). Aniade cobertura sobre los primitivos del registry que
// no tenian su entry en la gallery.
#include "demos.h"
#include "demo.h"
#include "core/process_runner.h"
#include "viz/candlestick.h"
#include "viz/gauge.h"
#include "viz/heatmap.h"
#include "viz/table_view.h"
#include <imgui.h>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <thread>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// process_runner (Core)
// ---------------------------------------------------------------------------
void demo_process_runner() {
demo_header("process_runner", "v1.0.0",
"Ejecuta una tarea en std::thread en background y expone estado thread-safe "
"(idle/running/success/error). El widget runner_status() dibuja inline un "
"spinner mientras corre y un mensaje de Success/Error al terminar.");
static fn_ui::ProcessRunner runner;
section("Tarea simulada (sleep 2s)");
{
if (ImGui::Button("Run task")) {
if (!runner.is_busy()) {
fn_ui::runner_trigger(runner, [](std::string& out) -> bool {
std::this_thread::sleep_for(std::chrono::seconds(2));
out = "task done in 2s";
return true;
});
}
}
ImGui::SameLine();
if (ImGui::Button("Run failing task")) {
if (!runner.is_busy()) {
fn_ui::runner_trigger(runner, [](std::string& out) -> bool {
std::this_thread::sleep_for(std::chrono::seconds(1));
out = "simulated failure";
return false;
});
}
}
ImGui::SameLine();
if (ImGui::Button("Reset")) runner.reset();
fn_ui::runner_status(runner, "Working...");
}
code_block(
"static fn_ui::ProcessRunner r;\n"
"if (button(\"Run\", Primary) && !r.is_busy()) {\n"
" fn_ui::runner_trigger(r, [](std::string& out) -> bool {\n"
" return do_work(&out);\n"
" });\n"
"}\n"
"fn_ui::runner_status(r, \"Working...\");"
);
}
// ---------------------------------------------------------------------------
// candlestick (Viz)
// ---------------------------------------------------------------------------
void demo_candlestick() {
demo_header("candlestick", "v1.0.0",
"Grafico de velas OHLC con ImPlot custom rendering. Verde si close >= open, "
"rojo si bajista. Tooltip al hover muestra OHLC del dia.");
section("OHLC sintetico (30 dias)");
{
static std::vector<double> dates, opens, closes, lows, highs;
if (dates.empty()) {
dates.reserve(30); opens.reserve(30); closes.reserve(30);
lows.reserve(30); highs.reserve(30);
double price = 100.0;
for (int i = 0; i < 30; ++i) {
double drift = std::sin(i * 0.4) * 1.2;
double o = price;
double c = price + drift + (i % 3 == 0 ? -0.6 : 0.4);
double l = std::min(o, c) - 0.8 - (i % 5) * 0.1;
double h = std::max(o, c) + 0.8 + (i % 4) * 0.1;
dates.push_back(double(i));
opens.push_back(o);
closes.push_back(c);
lows.push_back(l);
highs.push_back(h);
price = c;
}
}
candlestick("##cs", dates.data(), opens.data(), closes.data(),
lows.data(), highs.data(), int(dates.size()));
}
code_block(
"candlestick(\"##cs\", dates, opens, closes, lows, highs, n,\n"
" /*width_percent=*/0.25f, /*tooltip=*/true);"
);
}
// ---------------------------------------------------------------------------
// gauge (Viz)
// ---------------------------------------------------------------------------
void demo_gauge() {
demo_header("gauge", "v1.0.0",
"Indicador circular tipo velocimetro con ImGui DrawList. Color interpolado "
"verde -> amarillo -> rojo segun el valor normalizado.");
static float v_cpu = 0.32f, v_mem = 0.78f, v_gpu = 0.55f;
section("Tres gauges con sliders");
{
ImGui::SliderFloat("cpu", &v_cpu, 0.0f, 1.0f);
ImGui::SliderFloat("mem", &v_mem, 0.0f, 1.0f);
ImGui::SliderFloat("gpu", &v_gpu, 0.0f, 1.0f);
ImGui::Spacing();
ImGui::BeginGroup();
gauge("CPU", v_cpu, 0.0f, 1.0f, 60.0f);
ImGui::EndGroup();
ImGui::SameLine(0.0f, 24.0f);
ImGui::BeginGroup();
gauge("MEM", v_mem, 0.0f, 1.0f, 60.0f);
ImGui::EndGroup();
ImGui::SameLine(0.0f, 24.0f);
ImGui::BeginGroup();
gauge("GPU", v_gpu, 0.0f, 1.0f, 60.0f);
ImGui::EndGroup();
}
code_block("gauge(\"CPU\", 0.32f, 0.0f, 1.0f, 60.0f);");
}
// ---------------------------------------------------------------------------
// heatmap (Viz)
// ---------------------------------------------------------------------------
void demo_heatmap() {
demo_header("heatmap", "v1.0.0",
"Mapa de calor 2D con ImPlot. Datos row-major. Util para correlation "
"matrices, attention maps, distribuciones 2D discretas.");
constexpr int R = 12;
constexpr int C = 12;
static float values[R * C] = {0};
static bool init = false;
if (!init) {
for (int r = 0; r < R; ++r) {
for (int c = 0; c < C; ++c) {
float dx = (c - C * 0.5f) / float(C);
float dy = (r - R * 0.5f) / float(R);
values[r * C + c] = std::exp(-(dx * dx + dy * dy) * 6.0f);
}
}
init = true;
}
section("Gaussian 12x12");
{
heatmap("##hm", values, R, C, 0.0f, 1.0f);
}
code_block(
"float values[R * C];\n"
"// fill row-major: values[r * C + c] = ...\n"
"heatmap(\"##hm\", values, R, C, /*min=*/0.0f, /*max=*/1.0f);"
);
}
// ---------------------------------------------------------------------------
// table_view (Viz)
// ---------------------------------------------------------------------------
void demo_table_view() {
demo_header("table_view", "v1.0.0",
"Tabla interactiva con sorting indicators y scroll usando la ImGui Tables API. "
"Headers + cells row-major. Util para dashboards y inspectores.");
section("Lenguajes del registry");
{
const char* headers[] = {"id", "lang", "domain", "purity"};
// 6 filas x 4 cols, row-major
const char* cells[] = {
"filter_slice_go_core", "go", "core", "pure",
"metabase_setup_py_infra", "py", "infra", "impure",
"rsync_deploy_bash_infra", "sh", "infra", "impure",
"button_cpp_core", "cpp", "core", "pure",
"gl_texture_load_cpp_gfx", "cpp", "gfx", "impure",
"audio_fft_cpp_core", "cpp", "core", "pure",
};
const int row_count = 6;
const int col_count = 4;
table_view("##tbl", headers, col_count, cells, row_count);
}
code_block(
"const char* headers[] = {\"id\", \"lang\", \"domain\"};\n"
"const char* cells[] = {/* row-major: r0c0,r0c1,r0c2, r1c0,... */};\n"
"table_view(\"##tbl\", headers, 3, cells, n_rows);"
);
}
} // namespace gallery
-196
View File
@@ -1,196 +0,0 @@
// Demos del dominio gfx — primitivos OpenGL/shader que viven en
// cpp/functions/gfx/. La pieza distintiva de shaders_lab es el
// shader_canvas: framebuffer + fullscreen quad + programa GL animado por
// time/resolution/mouse.
#include "demos.h"
#include "demo.h"
#include "gfx/shader_canvas.h"
#include "gfx/gl_shader.h"
#include "gfx/gl_loader.h"
#include <imgui.h>
#include <chrono>
namespace gallery {
namespace {
// Fragment shader sintetico — gradiente animado con celdas. Usa los uniforms
// estandar que compile_fragment inyecta: u_resolution, u_time, u_mouse.
const char* kShaderSrc = R"(
void mainImage() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec2 cell = uv * 8.0;
vec2 ipos = floor(cell);
vec2 fpos = fract(cell) - 0.5;
float t = u_time * 0.6;
float wave = sin(ipos.x * 0.7 + ipos.y * 0.5 + t);
float dist = length(fpos);
vec3 a = vec3(0.30, 0.43, 0.96); // indigo
vec3 b = vec3(0.95, 0.45, 0.85); // pink
vec3 col = mix(a, b, 0.5 + 0.5 * wave);
// Mouse focus: oscurecemos celdas lejanas al cursor.
vec2 m = u_mouse / u_resolution;
float fm = 1.0 - smoothstep(0.0, 0.6, length(uv - m));
col *= 0.6 + 0.4 * fm;
// Disco interior por celda con borde suave.
col *= smoothstep(0.5, 0.45, dist);
fragColor = vec4(col, 1.0);
}
void main() {
mainImage();
}
)";
struct CanvasState {
fn::gfx::ShaderCanvas canvas;
bool compiled = false;
bool compile_failed = false;
std::string err_msg;
std::chrono::steady_clock::time_point t0;
};
CanvasState& state() {
static CanvasState s;
return s;
}
} // namespace
void demo_shader_canvas() {
demo_header("shader_canvas", "v1.0.0",
"Framebuffer + fullscreen quad + shader GLSL animado. La misma pieza "
"que usa shaders_lab para el preview en vivo. Uniforms u_time / u_resolution / u_mouse "
"los inyecta gl_shader::compile_fragment automaticamente.");
auto& s = state();
// Compilacion lazy (en el primer frame ya hay contexto GL valido).
if (!s.compiled && !s.compile_failed) {
fn::gfx::gl_loader_init();
fn::gfx::canvas_init(s.canvas);
auto cr = fn::gfx::compile_fragment(kShaderSrc);
if (!cr.ok) {
s.compile_failed = true;
s.err_msg = cr.err_msg;
} else {
fn::gfx::canvas_set_program(s.canvas, cr.program);
s.t0 = std::chrono::steady_clock::now();
s.compiled = true;
}
}
if (s.compile_failed) {
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1),
"Compilacion del fragment shader fallo:\n%s",
s.err_msg.c_str());
return;
}
section("Live preview");
// Render del shader en un panel ~480x300 px. canvas_render hace resize
// automatico segun GetContentRegionAvail si lo dejas crecer.
ImGui::BeginChild("##shader_preview", ImVec2(480, 300),
ImGuiChildFlags_Borders);
const float dt = std::chrono::duration<float>(
std::chrono::steady_clock::now() - s.t0).count();
fn::gfx::canvas_render(s.canvas, dt);
ImGui::EndChild();
code_block(
"#include \"gfx/shader_canvas.h\"\n"
"#include \"gfx/gl_shader.h\"\n\n"
"static fn::gfx::ShaderCanvas canvas;\n"
"// Setup (una vez):\n"
"fn::gfx::canvas_init(canvas);\n"
"auto cr = fn::gfx::compile_fragment(user_glsl);\n"
"if (cr.ok) fn::gfx::canvas_set_program(canvas, cr.program);\n\n"
"// Cada frame, dentro de un Begin/End:\n"
"fn::gfx::canvas_render(canvas, time_seconds);"
);
}
// Issue 0049b — Mostrar la version de OpenGL del contexto y un puñado de
// limites 4.3 que confirman que compute shaders / SSBO / image load-store
// estan disponibles. No es codigo del registry, solo introspeccion del
// driver — sin estado, sin side effects: solo glGetString + glGetIntegerv.
#ifndef GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS
#define GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS 0x90DD
#endif
#ifndef GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS
#define GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS 0x90DC
#endif
#ifndef GL_MAX_COMPUTE_SHARED_MEMORY_SIZE
#define GL_MAX_COMPUTE_SHARED_MEMORY_SIZE 0x8262
#endif
void demo_gl_info() {
demo_header("gl_info", "v1.0.0",
"Introspeccion del contexto OpenGL activo (issue 0049b). El framework "
"ahora pide GL 4.3 core, lo que habilita compute shaders, SSBOs, image "
"load/store y atomic counters — bloques esenciales del graph_renderer "
"GPU del proyecto osint_graph.");
auto gl_str = [](GLenum e) -> const char* {
const GLubyte* s = glGetString(e);
return s ? reinterpret_cast<const char*>(s) : "(null)";
};
section("Driver");
ImGui::Text("Vendor: %s", gl_str(GL_VENDOR));
ImGui::Text("Renderer: %s", gl_str(GL_RENDERER));
ImGui::Text("Version: %s", gl_str(GL_VERSION));
ImGui::Text("GLSL: %s", gl_str(GL_SHADING_LANGUAGE_VERSION));
GLint major = 0, minor = 0;
glGetIntegerv(GL_MAJOR_VERSION, &major);
glGetIntegerv(GL_MINOR_VERSION, &minor);
const bool has_43 = (major > 4) || (major == 4 && minor >= 3);
section("Capabilities");
ImGui::Text("Context: %d.%d core", major, minor);
if (has_43) {
ImGui::TextColored(ImVec4(0.40f, 0.85f, 0.40f, 1.0f),
"OpenGL 4.3+ — compute shaders, SSBOs, image load/store, atomic counters: AVAILABLE");
} else {
ImGui::TextColored(ImVec4(0.95f, 0.55f, 0.30f, 1.0f),
"OpenGL < 4.3 — compute shaders / SSBOs NOT available on this driver");
}
section("Limits");
GLint v = 0;
auto row = [&](const char* label, GLenum e) {
v = 0;
glGetIntegerv(e, &v);
ImGui::Text("%-44s %d", label, v);
};
row("GL_MAX_TEXTURE_SIZE", GL_MAX_TEXTURE_SIZE);
row("GL_MAX_VERTEX_ATTRIBS", GL_MAX_VERTEX_ATTRIBS);
row("GL_MAX_UNIFORM_BLOCK_SIZE", GL_MAX_UNIFORM_BLOCK_SIZE);
if (has_43) {
row("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS);
row("GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS);
row("GL_MAX_COMPUTE_SHARED_MEMORY_SIZE", GL_MAX_COMPUTE_SHARED_MEMORY_SIZE);
}
code_block(
"// Solo glGetString + glGetIntegerv — sin loader extra.\n"
"GLint major = 0, minor = 0;\n"
"glGetIntegerv(GL_MAJOR_VERSION, &major);\n"
"glGetIntegerv(GL_MINOR_VERSION, &minor);\n"
"bool has_compute = (major > 4) || (major == 4 && minor >= 3);"
);
}
} // namespace gallery
@@ -1,127 +0,0 @@
// Demo de gl_texture_load (cpp/functions/gfx/gl_texture_load.{h,cpp}).
// Carga assets/sample.png y lo muestra con ImGui::Image. Sliders para tint
// RGB que se aplican como modulacion (ImGui::Image acepta tint_col).
//
// Limitacion: el "zoom UV" se simula moviendo uv0/uv1 (que ImGui::Image acepta
// nativamente). Asi evitamos compilar un shader custom adicional para la demo.
#include "demos.h"
#include "demo.h"
#include "gfx/gl_texture_load.h"
#include "gfx/gl_loader.h"
#include <imgui.h>
#include <cstdio>
#include <cstring>
namespace gallery {
namespace {
struct TextureState {
fn::GlTexture tex{};
bool tried_load = false;
std::string_view err;
char err_buf[256] = {0};
float tint[3] = {1.0f, 1.0f, 1.0f};
float zoom = 1.0f; // 1.0 = sin zoom; >1 hace UV mas pequeno
};
TextureState& state() {
static TextureState s;
return s;
}
// Resuelve un path para el asset. Probamos varios candidatos relativos al cwd
// del binario (puede lanzarse desde build/ o desde la raiz del repo).
const char* resolve_sample_path() {
static const char* candidates[] = {
"assets/sample.png",
"apps/primitives_gallery/assets/sample.png",
"cpp/apps/primitives_gallery/assets/sample.png",
"../cpp/apps/primitives_gallery/assets/sample.png",
"../../cpp/apps/primitives_gallery/assets/sample.png",
"../../../cpp/apps/primitives_gallery/assets/sample.png",
nullptr,
};
for (int i = 0; candidates[i]; i++) {
FILE* f = std::fopen(candidates[i], "rb");
if (f) { std::fclose(f); return candidates[i]; }
}
return candidates[0]; // devolver el primer candidato para que el error sea mas descriptivo
}
} // namespace
void demo_gl_texture() {
demo_header("gl_texture_load", "v1.0.0",
"Carga PNG/JPG/HDR desde disco a una textura GL lista para sampler2D. "
"Vendorea stb_image (cpp/vendor/stb/). Demo: assets/sample.png "
"(damero 256x256), tint RGB modulando ImGui::Image, zoom UV.");
auto& s = state();
if (!s.tried_load) {
// Asegurar simbolos GL resueltos (Linux no-op, Windows wglGetProcAddress).
fn::gfx::gl_loader_init();
const char* path = resolve_sample_path();
s.tex = fn::gl_texture_load(path, /*flip_y=*/true, /*srgb=*/false);
if (!s.tex.ok()) {
std::snprintf(s.err_buf, sizeof(s.err_buf),
"no se pudo cargar '%s': %s",
path, fn::gl_texture_last_error());
}
s.tried_load = true;
}
if (!s.tex.ok()) {
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", s.err_buf);
ImGui::TextWrapped(
"El binario busca el PNG en varios paths relativos al cwd. "
"Lanzar desde la raiz del repo o desde cpp/build/ deberia funcionar.");
return;
}
section("Texture info");
ImGui::Text("size: %d x %d px", s.tex.w, s.tex.h);
ImGui::Text("channels: %d (forzado a RGBA en upload)", s.tex.channels);
ImGui::Text("gl_id: %u", (unsigned)s.tex.id);
section("Tint + zoom");
ImGui::SliderFloat3("tint RGB", s.tint, 0.0f, 2.0f, "%.2f");
ImGui::SliderFloat("zoom UV", &s.zoom, 0.25f, 4.0f, "%.2fx");
section("Preview");
// Calcular UVs centradas con zoom: 1.0 = (0,0)-(1,1), 2.0 = (0.25,0.25)-(0.75,0.75)
float u_half = 0.5f / (s.zoom > 0.001f ? s.zoom : 0.001f);
ImVec2 uv0(0.5f - u_half, 0.5f - u_half);
ImVec2 uv1(0.5f + u_half, 0.5f + u_half);
ImVec4 tint(s.tint[0], s.tint[1], s.tint[2], 1.0f);
// Conversion GLuint -> ImTextureID. ImGui::Image acepta cualquier id de
// textura del backend; en imgui_impl_opengl3 es directamente el GLuint.
ImTextureID tid = (ImTextureID)(intptr_t)s.tex.id;
ImGui::ImageWithBg(tid, ImVec2(384.0f, 384.0f), uv0, uv1,
ImVec4(0, 0, 0, 0), tint);
code_block(
"#include \"gfx/gl_texture_load.h\"\n\n"
"auto tex = fn::gl_texture_load(\"assets/sample.png\");\n"
"if (!tex.ok()) {\n"
" fprintf(stderr, \"%s\\n\", fn::gl_texture_last_error());\n"
" return 1;\n"
"}\n"
"// uso en shader:\n"
"glUseProgram(prog);\n"
"fn::gl_texture_bind_uniform(prog, \"u_tex\", tex, /*unit=*/0);\n"
"glDrawArrays(GL_TRIANGLES, 0, 6);\n\n"
"// o en ImGui directamente:\n"
"ImGui::Image((ImTextureID)(intptr_t)tex.id, ImVec2(w, h));"
);
}
} // namespace gallery
-443
View File
@@ -1,443 +0,0 @@
#include "demos.h"
#include "demo.h"
#include "viz/graph_types.h"
#include "viz/graph_viewport.h"
#include "viz/graph_force_layout.h"
#include "viz/graph_force_layout_gpu.h"
#include "viz/graph_layouts.h"
#include "viz/graph_labels.h"
#include "core/button.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cmath>
#include <cstdio>
#include <vector>
namespace gallery {
// Paleta del demo: 8 colores tipo Mantine. v2.0 los usamos a traves de la
// tabla EntityType en lugar de escribirlos por nodo. Asi el modelo nuevo
// queda demostrado tal cual lo van a usar las apps reales (osint_graph,
// fn_explorer): tabla pequena de tipos + nodos que solo guardan type_id.
static const uint32_t k_demo_palette[] = {
0xFFEF8D5Bu, 0xFF8CCA58u, 0xFF3E97F5u, 0xFF5051D9u,
0xFFE07FB8u, 0xFFCCCD5Fu, 0xFF52CDF2u, 0xFF61D199u,
};
static constexpr int k_demo_palette_n =
sizeof(k_demo_palette) / sizeof(k_demo_palette[0]);
// Tabla compartida entre regeneraciones — las apariencias no cambian aunque
// el usuario regenere el grafo, asi que vive como `static`.
static EntityType s_demo_entity_types[k_demo_palette_n];
static RelationType s_demo_relation_types[1];
static bool s_demo_types_initialized = false;
static void init_demo_types() {
if (s_demo_types_initialized) return;
for (int k = 0; k < k_demo_palette_n; ++k) {
s_demo_entity_types[k] = entity_type(k_demo_palette[k],
SHAPE_CIRCLE, 4.0f, "cluster");
}
s_demo_relation_types[0] = relation_type(0xFF888888u, EDGE_SOLID, 1.0f, "default");
s_demo_types_initialized = true;
}
// Genera un grafo sintetico con N nodos en K clusters. Cada nodo tiene
// `edges_per_node` aristas intra-cluster + un pct% global inter-cluster.
// Cluster radio escala con sqrt(N) para que la "nube" no sea siempre el
// mismo cuadrado de 200 px — a 1M nodos crece a ~6 km de radio en graph
// space y los nodos pueden esparcirse libremente sin caja artificial.
static void generate_synthetic_graph(int N, int K,
int edges_per_node, int inter_pct,
std::vector<GraphNode>& nodes_out,
std::vector<GraphEdge>& edges_out) {
nodes_out.clear();
edges_out.clear();
nodes_out.reserve(N);
edges_out.reserve((size_t)N * (size_t)edges_per_node + (size_t)N * (size_t)inter_pct / 100u);
unsigned seed = 0x1234abcd;
auto rnd = [&]() {
seed = seed * 1664525u + 1013904223u;
return static_cast<float>((seed >> 8) & 0xffffff) / 16777216.0f;
};
// Cluster radius y scatter escalan con sqrt(N) para que los nodos no
// queden empaquetados al subir el slider. A 1M nodes el espacio inicial
// es ~12k px de lado en lugar de los 280 px hardcoded de antes.
const float scale = std::sqrt(static_cast<float>(std::max(N, 1)));
const float cluster_r = 12.0f * scale;
const float scatter = 4.0f * scale;
std::vector<float> cluster_cx(K), cluster_cy(K);
for (int k = 0; k < K; k++) {
float angle = 2.0f * 3.14159f * k / K;
cluster_cx[k] = std::cos(angle) * cluster_r;
cluster_cy[k] = std::sin(angle) * cluster_r;
}
for (int i = 0; i < N; i++) {
int k = i % K;
// type_id mapea al EntityType (k % k_demo_palette_n) que define
// color y shape. size_override = 3..5 px para conservar la
// variacion sutil del demo v1 — apariencia visual identica.
uint16_t tid = static_cast<uint16_t>(k % k_demo_palette_n);
GraphNode n = graph_node(
cluster_cx[k] + (rnd() - 0.5f) * scatter,
cluster_cy[k] + (rnd() - 0.5f) * scatter,
tid);
n.size_override = 3.0f + rnd() * 2.0f;
n.user_data = static_cast<uint64_t>(i);
nodes_out.push_back(n);
}
auto add_edge = [&](uint32_t a, uint32_t b, float w) {
if (a == b) return;
edges_out.push_back(graph_edge(a, b, w));
};
int per_cluster = N / K;
for (int k = 0; k < K; k++) {
int base = k * per_cluster;
int end = (k == K - 1) ? N : (base + per_cluster);
int size = end - base;
if (size < 2) continue;
for (int i = base; i < end; i++) {
for (int e = 0; e < edges_per_node; e++) {
int j = base + static_cast<int>(rnd() * size);
add_edge(static_cast<uint32_t>(i),
static_cast<uint32_t>(j), 1.0f);
}
}
}
// Inter-cluster: pct% del total de nodos
long long inter = (long long)N * (long long)inter_pct / 100LL;
for (long long e = 0; e < inter; e++) {
uint32_t a = static_cast<uint32_t>(rnd() * N);
uint32_t b = static_cast<uint32_t>(rnd() * N);
add_edge(a, b, 0.3f);
}
}
void demo_graph() {
demo_header("graph_viewport", "v1.0.0",
"Pipeline completo de visualizacion de grafos: graph_renderer (instanced GPU) "
"+ graph_force_layout (Barnes-Hut) + graph_spatial_hash (hit-testing). "
"Render a FBO mostrado via ImGui::Image — escala a decenas de miles de nodos.");
static int s_n_nodes = 1000;
static int s_n_clusters = 6;
static int s_edges_per_n = 3; // aristas intra-cluster por nodo
static int s_inter_pct = 5; // % de nodos para edges inter-cluster
static float s_repulsion = 3500.0f; // fuerza de dispersion entre nodos
static float s_attraction = 0.02f; // muelle entre nodos conectados
static float s_gravity = 0.001f; // tiron hacia el centro
static std::vector<GraphNode> s_nodes;
static std::vector<GraphEdge> s_edges;
static GraphData s_graph{};
static GraphViewportState s_state;
static bool s_initialized = false;
static bool s_needs_regen = true;
// GPU layout (issue 0049h): toggle CPU/GPU. ctx se crea perezosamente al
// primer frame en GPU mode; max_nodes/max_edges se dimensionan al maximo
// que ofrece el slider (1M nodos x 10 edges/nodo = 10M edges) — los SSBOs
// ocupan ~80 MB en ese tope, suficientemente barato para no
// recrear el ctx cada Regenerate. Si compute no esta disponible, el
// toggle queda deshabilitado.
static bool s_use_gpu = false;
static ForceLayoutGPU* s_gpu_ctx = nullptr;
static bool s_gpu_dirty = true; // re-upload tras regen / cambio
// Layout estatico activo (issue 0049i). 0=force (iterativo), 1=grid,
// 2=circular, 3=radial, 4=hierarchical, 5=fixed.
static int s_layout_mode = 0;
const char* k_layout_names[] = {
"force", "grid", "circular", "radial", "hierarchical", "fixed"
};
static int s_apply_layout = 0; // se incrementa cuando hay que reaplicar
// Labels (issue 0049j). LabelPolicy controlable desde la UI.
static graph::LabelPolicy s_label_policy;
static bool s_labels_enabled = true;
if (s_needs_regen) {
init_demo_types();
generate_synthetic_graph(s_n_nodes, s_n_clusters,
s_edges_per_n, s_inter_pct,
s_nodes, s_edges);
s_graph.nodes = s_nodes.data();
s_graph.node_count = static_cast<int>(s_nodes.size());
s_graph.node_capacity = static_cast<int>(s_nodes.capacity());
s_graph.edges = s_edges.data();
s_graph.edge_count = static_cast<int>(s_edges.size());
s_graph.edge_capacity = static_cast<int>(s_edges.capacity());
s_graph.types = s_demo_entity_types;
s_graph.type_count = k_demo_palette_n;
s_graph.rel_types = s_demo_relation_types;
s_graph.rel_type_count = 1;
s_graph.update_bounds();
s_state.layout_running = true;
s_state.layout_energy = 0.0f;
s_needs_regen = false;
s_initialized = true;
s_gpu_dirty = true;
}
section("Controls");
{
using namespace fn_ui;
ImGui::PushItemWidth(180);
// Slider Nodes con escala logaritmica para que sea util tanto a 100
// como a 1M sin tener que arrastrar 10000px.
ImGui::SliderInt("Nodes", &s_n_nodes, 100, 1000000, "%d",
ImGuiSliderFlags_Logarithmic);
ImGui::SameLine();
ImGui::SliderInt("Clusters", &s_n_clusters, 2, 16);
ImGui::SliderInt("Edges/node", &s_edges_per_n, 1, 10);
ImGui::SameLine();
ImGui::SliderInt("Inter %", &s_inter_pct, 0, 30, "%d%%");
ImGui::SliderFloat("Repulsion", &s_repulsion, 100.0f, 20000.0f, "%.0f");
ImGui::SameLine();
ImGui::SliderFloat("Attraction", &s_attraction, 0.001f, 0.5f, "%.3f");
ImGui::SameLine();
ImGui::SliderFloat("Gravity", &s_gravity, 0.0f, 0.05f, "%.4f");
ImGui::PopItemWidth();
if (button("Regenerate", ButtonVariant::Primary)) s_needs_regen = true;
ImGui::SameLine();
if (button(s_state.layout_running ? "Pause layout" : "Resume layout",
ButtonVariant::Secondary)) {
s_state.layout_running = !s_state.layout_running;
}
ImGui::SameLine();
if (button("Fit view", ButtonVariant::Subtle)) {
graph_viewport_fit(s_graph, s_state);
}
ImGui::SameLine();
// Toggle GPU layout. Si compute no esta disponible (Mesa software o
// driver < 4.3), deshabilitamos visualmente el checkbox.
bool prev_gpu = s_use_gpu;
if (s_gpu_ctx == nullptr && s_use_gpu == false) {
// primera oportunidad: intentar crear el ctx para detectar soporte.
// Lazy init solo si el usuario lo activa.
}
ImGui::Checkbox("GPU layout", &s_use_gpu);
if (s_use_gpu != prev_gpu) {
s_gpu_dirty = true; // re-upload al cambiar de modo
}
// Selector de layout (issue 0049i).
ImGui::PushItemWidth(140);
int prev_mode = s_layout_mode;
if (ImGui::Combo("Layout", &s_layout_mode,
k_layout_names, IM_ARRAYSIZE(k_layout_names))) {
// Cambio de modo: reaplicar instantaneamente
s_apply_layout++;
}
if (prev_mode != s_layout_mode) {
// En "force" volvemos a animar; en cualquier estatico paramos.
s_state.layout_running = (s_layout_mode == 0);
}
ImGui::PopItemWidth();
ImGui::SameLine();
if (button("Apply layout", ButtonVariant::Subtle)) s_apply_layout++;
// --- Labels (issue 0049j) ---------------------------------------
ImGui::Checkbox("Labels", &s_labels_enabled);
ImGui::SameLine();
ImGui::PushItemWidth(140);
ImGui::SliderInt("Max visible", &s_label_policy.max_visible, 0, 1000);
ImGui::SameLine();
ImGui::SliderFloat("Font", &s_label_policy.font_size,
8.0f, 24.0f, "%.0f");
ImGui::SameLine();
ImGui::SliderFloat("Min px", &s_label_policy.min_node_pixel_size,
0.0f, 40.0f, "%.0f");
ImGui::PopItemWidth();
ImGui::SameLine();
ImGui::Checkbox("Selected", &s_label_policy.always_for_selected);
ImGui::SameLine();
ImGui::Checkbox("Hovered", &s_label_policy.always_for_hovered);
ImGui::SameLine();
ImGui::Checkbox("Pinned", &s_label_policy.always_for_pinned);
}
section("Stats");
{
// Una sola linea fija — sin secciones condicionales que cambien la
// altura del panel (eso provocaba que el viewport saltara al hacer
// hover/select).
char hover_buf[32];
char sel_buf[32];
if (s_state.hovered_node >= 0) {
std::snprintf(hover_buf, sizeof(hover_buf), "#%d t%u",
s_state.hovered_node,
(unsigned)s_nodes[s_state.hovered_node].type_id);
} else {
std::snprintf(hover_buf, sizeof(hover_buf), "-");
}
if (s_state.selected_node >= 0) {
std::snprintf(sel_buf, sizeof(sel_buf), "#%d", s_state.selected_node);
} else {
std::snprintf(sel_buf, sizeof(sel_buf), "-");
}
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
ImGui::Text("nodes=%d edges=%d energy=%.2f fps=%.0f | hover=%s sel=%s",
s_graph.node_count, s_graph.edge_count,
s_state.layout_energy, ImGui::GetIO().Framerate,
hover_buf, sel_buf);
ImGui::PopStyleColor();
}
// Aplicar layout estatico cuando se solicita (cambio de modo / boton).
static int s_last_apply = -1;
if (s_apply_layout != s_last_apply) {
s_last_apply = s_apply_layout;
switch (s_layout_mode) {
case 1: graph::layout_grid (s_graph, 25.0f); break;
case 2: graph::layout_circular (s_graph, 200.0f); break;
case 3: graph::layout_radial (s_graph, 0, 80.0f); break;
case 4: graph::layout_hierarchical(s_graph, 0, 120.0f, 50.0f); break;
case 5: graph::layout_fixed (s_graph); break;
case 0: default:
// force: dejar las posiciones actuales; el bucle lo refinara
break;
}
s_gpu_dirty = true;
if (s_layout_mode != 0) graph_viewport_fit(s_graph, s_state);
}
section("Viewport (drag=pan, wheel=zoom, click=select, shift+drag=lasso, ctrl+click=toggle)");
if (s_initialized) {
// Avanzamos 1 paso de force layout cada frame mientras layout_running.
// Auto-pause: si la energia por nodo cae bajo el umbral durante N
// frames consecutivos, paramos la simulacion automaticamente — el
// grafo ya esta estable. El usuario lo retoma con "Resume layout"
// o "Regenerate".
static int s_low_energy_frames = 0;
const int k_pause_after_frames = 30;
const float k_pause_per_node = 0.001f; // umbral de energia/nodo
if (s_state.layout_running && s_layout_mode == 0) {
ForceLayoutConfig cfg;
cfg.repulsion = s_repulsion;
cfg.attraction = s_attraction;
cfg.gravity = s_gravity;
cfg.iterations = 1;
if (s_use_gpu) {
if (!s_gpu_ctx) {
s_gpu_ctx = graph_force_layout_gpu_create(s_graph.node_count + 1024,
s_graph.edge_count + 1024);
s_gpu_dirty = true;
}
if (s_gpu_ctx) {
if (s_gpu_dirty) {
graph_force_layout_gpu_upload(s_gpu_ctx, s_graph);
s_gpu_dirty = false;
}
s_state.layout_energy = graph_force_layout_gpu_step(s_gpu_ctx, cfg);
graph_force_layout_gpu_readback(s_gpu_ctx, s_graph, /*include_velocities=*/true);
} else {
// GPU no disponible: caer a CPU silenciosamente.
s_use_gpu = false;
s_state.layout_energy = graph_force_layout_step(s_graph, cfg);
}
} else {
s_state.layout_energy = graph_force_layout_step(s_graph, cfg);
}
const float per_node = s_graph.node_count > 0
? s_state.layout_energy / (float)s_graph.node_count
: 0.0f;
if (per_node < k_pause_per_node) ++s_low_energy_frames;
else s_low_energy_frames = 0;
if (graph_force_layout_should_pause(s_low_energy_frames,
k_pause_after_frames)) {
s_state.layout_running = false;
s_low_energy_frames = 0;
}
} else {
s_low_energy_frames = 0;
}
// Callbacks (issue 0049i): right-click abre popup contextual,
// double-click loguea el indice. Los callbacks corren dentro del
// frame ImGui — el caller puede usar OpenPopup directamente.
static int s_ctx_node = -1;
static bool s_ctx_open = false;
struct Cb {
static void on_ctx(int idx, ImVec2 /*pos*/, void* user) {
int* slot = (int*)user;
*slot = idx;
ImGui::OpenPopup("##graph_node_ctx");
}
static void on_dbl(int idx, void* /*user*/) {
std::printf("[graph] dbl-click on node %d\n", idx);
}
};
GraphViewportCallbacks cb;
cb.on_context_menu = &Cb::on_ctx;
cb.on_double_click = &Cb::on_dbl;
cb.user = &s_ctx_node;
graph_viewport("##graph_demo", s_graph, s_state, ImVec2(0, 460), cb);
// Labels overlay (issue 0049j). El callback formatea "#<idx>" en un
// buffer estatico por demo — apps reales (osint_graph) usaran un
// string pool de la BD origen.
if (s_labels_enabled) {
struct LblCtx { char buf[32]; };
static LblCtx s_lbl_ctx;
auto get_label = [](int idx, void* user) -> const char* {
auto* ctx = static_cast<LblCtx*>(user);
std::snprintf(ctx->buf, sizeof(ctx->buf), "#%d", idx);
return ctx->buf;
};
graph::graph_labels_draw(s_graph, s_state, s_label_policy,
get_label, &s_lbl_ctx);
}
if (ImGui::BeginPopup("##graph_node_ctx")) {
ImGui::Text("Node #%d", s_ctx_node);
ImGui::Separator();
if (ImGui::MenuItem("Pin")) {
if (s_ctx_node >= 0 && s_ctx_node < s_graph.node_count)
s_graph.nodes[s_ctx_node].flags |= NF_PINNED;
}
if (ImGui::MenuItem("Unpin")) {
if (s_ctx_node >= 0 && s_ctx_node < s_graph.node_count)
s_graph.nodes[s_ctx_node].flags &= ~NF_PINNED;
}
if (ImGui::MenuItem("Add to selection")) {
graph_viewport_add_to_selection(s_graph, s_state, s_ctx_node);
}
ImGui::EndPopup();
}
// Overlay con count seleccionados (lasso/multi-select feedback).
if (!s_state.selection.empty()) {
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text);
ImGui::Text("[%zu selected]", s_state.selection.size());
ImGui::PopStyleColor();
}
}
code_block(
"static GraphData graph;\n"
"static GraphViewportState state;\n"
"// ... rellenar graph.nodes / graph.edges ...\n"
"graph.update_bounds();\n"
"\n"
"// Por frame:\n"
"if (state.layout_running) {\n"
" ForceLayoutConfig cfg;\n"
" cfg.repulsion = 3500; cfg.gravity = 0.001f;\n"
" graph_force_layout_step(graph, cfg);\n"
"}\n"
"graph_viewport(\"##g\", graph, state, ImVec2(0, 460));"
);
}
} // namespace gallery
@@ -1,243 +0,0 @@
#include "demos.h"
#include "demo.h"
#include "viz/graph_types.h"
#include "viz/graph_viewport.h"
#include "viz/graph_renderer.h"
#include "viz/graph_force_layout.h"
#include "viz/graph_icons.h"
#include "core/button.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cmath>
#include <cstdio>
#include <vector>
namespace gallery {
// 6 codepoints Tabler representativos para los 6 EntityTypes del demo. El
// orden coincide con `s_entity_types[i]`: cada tipo apunta a `icon_id = i+1`
// (las regiones del atlas son 1-indexed; 0 reservado para "sin icono").
static const uint16_t k_demo_codepoints[6] = {
0xEB4Du, // TI_USER
0xEAE5u, // TI_MAIL
0xEAB9u, // TI_GLOBE
0xEB09u, // TI_PHONE
0xEA4Fu, // TI_BUILDING
0xEA88u, // TI_DATABASE
};
static const uint32_t k_styles_palette[6] = {
0xFF6BCB77u, // verde — Person (circle)
0xFFFF6B6Bu, // rojo — Email (square)
0xFF4D96FFu, // azul — Domain (diamond)
0xFFFFC75Fu, // ambar — Phone (hex)
0xFFC780E8u, // morado — Org (triangle)
0xFF52CDF2u, // cyan — Database (rounded square)
};
static const char* k_styles_names[6] = {
"Person", "Email", "Domain", "Phone", "Organization", "Database"
};
static EntityType s_entity_types[6];
static RelationType s_relation_types[3]; // solid, dashed, dotted
static IconAtlas* s_atlas = nullptr;
static bool s_types_initialized = false;
static bool s_atlas_bound = false;
static void init_demo_types() {
if (s_types_initialized) return;
for (int i = 0; i < 6; ++i) {
EntityType t{};
t.color = k_styles_palette[i];
t.shape = (uint8_t)(SHAPE_CIRCLE + i); // 1..6 — uno por shape
t.icon_id = (uint16_t)(i + 1); // 1-based
t.default_size = 14.0f;
t.name = k_styles_names[i];
s_entity_types[i] = t;
}
s_relation_types[0] = relation_type(0xFFCCCCCCu, EDGE_SOLID, 1.5f, "knows");
s_relation_types[1] = relation_type(0xFFFFB870u, EDGE_DASHED, 1.5f, "uses");
s_relation_types[2] = relation_type(0xFF89E0FCu, EDGE_DOTTED, 1.5f, "owns");
s_types_initialized = true;
}
// 30 nodos posicionados en un anillo por tipo. Aristas: cada nodo conecta a
// sus dos vecinos (arc) y a un nodo "central" del cluster siguiente. Mezcla
// de directed/undirected para validar las flechas.
static void build_demo_graph(std::vector<GraphNode>& nodes,
std::vector<GraphEdge>& edges)
{
nodes.clear();
edges.clear();
const int per_type = 5;
const float ring_r = 80.0f;
const float type_r = 30.0f;
for (int t = 0; t < 6; ++t) {
float ang_t = (float)t * (2.0f * 3.14159265f / 6.0f);
float cx = std::cos(ang_t) * ring_r;
float cy = std::sin(ang_t) * ring_r;
for (int k = 0; k < per_type; ++k) {
float a = (float)k * (2.0f * 3.14159265f / per_type) + ang_t * 0.3f;
GraphNode n = graph_node(cx + std::cos(a) * type_r,
cy + std::sin(a) * type_r,
(uint16_t)t);
n.user_data = (uint64_t)nodes.size();
nodes.push_back(n);
}
}
auto idx = [&](int t, int k) { return (uint32_t)(t * per_type + k); };
for (int t = 0; t < 6; ++t) {
// Aristas intra-cluster (knows = solid, undirected).
for (int k = 0; k < per_type; ++k) {
int next_k = (k + 1) % per_type;
GraphEdge e = graph_edge(idx(t, k), idx(t, next_k), 1.0f, /*type_id=*/0);
edges.push_back(e);
}
// Inter-cluster: del nodo 0 del cluster t al nodo 0 del cluster t+1
// como "uses" (dashed, directed).
int t_next = (t + 1) % 6;
GraphEdge e1 = graph_edge(idx(t, 0), idx(t_next, 0), 1.0f, /*type_id=*/1);
e1.flags |= EF_DIRECTED;
edges.push_back(e1);
// Y otra inter-cluster mas larga al cluster +2 como "owns" (dotted,
// directed). Asi se ven las 3 estilos a la vez.
int t_far = (t + 2) % 6;
GraphEdge e2 = graph_edge(idx(t, 2), idx(t_far, 3), 0.6f, /*type_id=*/2);
e2.flags |= EF_DIRECTED;
edges.push_back(e2);
}
}
void demo_graph_styles() {
demo_header("graph_renderer (shapes + icons + arrows + edge styles)", "v1.5.0",
"OSINT-style: 6 EntityTypes, uno por shape (circle, square, diamond, hex, "
"triangle, rounded square) con icono Tabler en el centro. 3 RelationTypes "
"(solid/dashed/dotted) con flechas en los aristas EF_DIRECTED. Mismas dos "
"draw calls que el viewport normal (1 nodos + 1 aristas).");
init_demo_types();
static std::vector<GraphNode> s_nodes;
static std::vector<GraphEdge> s_edges;
static GraphData s_graph{};
static GraphViewportState s_state;
static bool s_initialized = false;
static bool s_run_layout = false;
if (!s_initialized) {
build_demo_graph(s_nodes, s_edges);
s_graph.nodes = s_nodes.data();
s_graph.node_count = (int)s_nodes.size();
s_graph.node_capacity = (int)s_nodes.capacity();
s_graph.edges = s_edges.data();
s_graph.edge_count = (int)s_edges.size();
s_graph.edge_capacity = (int)s_edges.capacity();
s_graph.types = s_entity_types;
s_graph.type_count = 6;
s_graph.rel_types = s_relation_types;
s_graph.rel_type_count = 3;
s_graph.update_bounds();
s_state.layout_running = false; // queremos ver las shapes posicionadas, no el caos del force
s_state.zoom = 2.0f;
s_initialized = true;
}
section("Legend");
{
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
for (int i = 0; i < 6; ++i) {
ImGui::Text("%-13s shape=%d icon_id=%d color=#%06x",
k_styles_names[i],
(int)s_entity_types[i].shape,
(int)s_entity_types[i].icon_id,
(unsigned)(s_entity_types[i].color & 0x00FFFFFFu));
}
ImGui::Text("Edges: knows=solid, uses=dashed (directed), owns=dotted (directed)");
ImGui::PopStyleColor();
}
section("Controls");
{
using namespace fn_ui;
if (button(s_run_layout ? "Pause force layout" : "Run force layout",
ButtonVariant::Secondary)) {
s_run_layout = !s_run_layout;
s_state.layout_running = s_run_layout;
}
ImGui::SameLine();
if (button("Rebuild", ButtonVariant::Subtle)) {
build_demo_graph(s_nodes, s_edges);
s_graph.nodes = s_nodes.data();
s_graph.node_count = (int)s_nodes.size();
s_graph.edges = s_edges.data();
s_graph.edge_count = (int)s_edges.size();
s_graph.update_bounds();
}
ImGui::SameLine();
if (button("Fit view", ButtonVariant::Subtle)) {
graph_viewport_fit(s_graph, s_state);
}
}
section("Viewport");
if (s_run_layout) {
ForceLayoutConfig cfg;
cfg.repulsion = 1500.0f;
cfg.attraction = 0.04f;
cfg.gravity = 0.005f;
cfg.iterations = 1;
graph_force_layout_step(s_graph, cfg);
}
// El viewport crea internamente el GraphRenderer. La primera vez que se
// dibuja el panel, el renderer existe — bindeamos el atlas justo despues.
graph_viewport("##graph_styles", s_graph, s_state, ImVec2(0, 460));
if (!s_atlas_bound && s_state.renderer) {
s_atlas = graph_icons_build(k_demo_codepoints, 6, 32);
if (s_atlas) {
graph_renderer_set_icon_atlas(s_state.renderer,
graph_icons_texture(s_atlas),
graph_icons_uv_table(s_atlas),
graph_icons_count(s_atlas));
s_atlas_bound = true;
} else {
// Sin atlas: marcamos como bound para no reintentar cada frame —
// el renderer simplemente pinta sin overlay de iconos.
s_atlas_bound = true;
}
}
code_block(
"// Build atlas con 6 codepoints Tabler\n"
"const uint16_t cps[] = {0xEB4D, 0xEAE5, 0xEAB9, 0xEB09, 0xEA4F, 0xEA88};\n"
"IconAtlas* atlas = graph_icons_build(cps, 6, 32);\n"
"\n"
"// EntityTypes: cada uno con su shape e icono\n"
"EntityType person = {0xFF6BCB77, SHAPE_CIRCLE, /*icon_id=*/1, 14, \"Person\"};\n"
"EntityType email = {0xFFFF6B6B, SHAPE_SQUARE, /*icon_id=*/2, 14, \"Email\"};\n"
"// ... etc\n"
"\n"
"// RelationTypes: solid / dashed / dotted\n"
"RelationType knows = relation_type(0xFFCCCCCC, EDGE_SOLID, 1.5f, \"knows\");\n"
"RelationType uses = relation_type(0xFFFFB870, EDGE_DASHED, 1.5f, \"uses\");\n"
"\n"
"// Bind atlas al renderer\n"
"graph_renderer_set_icon_atlas(renderer, graph_icons_texture(atlas),\n"
" graph_icons_uv_table(atlas),\n"
" graph_icons_count(atlas));\n"
"\n"
"// Aristas direccionales\n"
"GraphEdge e = graph_edge(src, tgt, 1.0f, /*type_id=*/1);\n"
"e.flags |= EF_DIRECTED;");
}
} // namespace gallery
-108
View File
@@ -1,108 +0,0 @@
// Demo del primitivo viz/mesh_viewer.
// Genera un cubo procedural in-line, lo sube al GPU, y permite cargar un
// .obj desde un path ingresado en un text input.
#include "demos.h"
#include "demo.h"
#include "viz/mesh_viewer.h"
#include "gfx/mesh_obj_load.h"
#include "gfx/mesh_gpu.h"
#include "core/orbit_camera.h"
#include <imgui.h>
#include <cstring>
#include <string>
namespace gallery {
namespace {
const char* kCubeObj =
"v -1 -1 -1\nv 1 -1 -1\nv 1 1 -1\nv -1 1 -1\n"
"v -1 -1 1\nv 1 -1 1\nv 1 1 1\nv -1 1 1\n"
"f 4 3 2 1\n" // back (-Z) — winding for outward normal
"f 5 6 7 8\n" // front (+Z)
"f 1 2 6 5\n" // bottom (-Y)
"f 8 7 3 4\n" // top (+Y)
"f 5 8 4 1\n" // left (-X)
"f 2 3 7 6\n"; // right (+X)
struct State {
fn::gfx::MeshGpu mesh{};
fn::core::OrbitCamera cam{};
char path[512] = "";
std::string status;
bool wireframe = false;
bool initialized = false;
};
State& state() {
static State s;
return s;
}
void load_cube() {
auto& s = state();
if (s.mesh.ok()) fn::gfx::mesh_gpu_destroy(s.mesh);
auto cpu = fn::gfx::mesh_obj_parse(kCubeObj, std::strlen(kCubeObj));
s.mesh = fn::gfx::mesh_gpu_upload(cpu);
s.status = s.mesh.ok()
? ("loaded cube: " + std::to_string(s.mesh.index_count / 3) + " tris")
: "cube upload failed";
}
void load_from_path() {
auto& s = state();
if (!s.path[0]) { s.status = "path is empty"; return; }
auto cpu = fn::gfx::mesh_obj_load(s.path);
if (cpu.positions.empty()) { s.status = "parse/read failed"; return; }
if (s.mesh.ok()) fn::gfx::mesh_gpu_destroy(s.mesh);
s.mesh = fn::gfx::mesh_gpu_upload(cpu);
s.status = s.mesh.ok()
? ("loaded: " + std::to_string(s.mesh.index_count / 3) + " tris")
: "upload failed";
}
} // namespace
void demo_mesh_viewer() {
demo_header("mesh_viewer", "v1.0.0",
"Visualizador 3D para inspeccion de geometria. Composicion de "
"mesh_obj_load (parser .obj puro) + mesh_gpu (upload VAO/VBO/EBO) + "
"orbit_camera (drag/wheel) + mesh_viewer (FBO + ImGui::Image + Lambert).");
auto& s = state();
if (!s.initialized) {
load_cube();
s.initialized = true;
}
// Controls row.
if (ImGui::Button("Reload cube")) load_cube();
ImGui::SameLine();
ImGui::Checkbox("Wireframe", &s.wireframe);
ImGui::SameLine();
ImGui::TextDisabled("|");
ImGui::SameLine();
ImGui::SetNextItemWidth(360);
ImGui::InputTextWithHint("##obj_path", "absolute path to .obj", s.path, sizeof(s.path));
ImGui::SameLine();
if (ImGui::Button("Load .obj")) load_from_path();
ImGui::TextDisabled("status: %s | tris: %d | drag to orbit, wheel to zoom",
s.status.c_str(),
s.mesh.ok() ? s.mesh.index_count / 3 : 0);
ImGui::Separator();
fn::viz::MeshViewerConfig cfg{};
cfg.mesh = &s.mesh;
cfg.cam = &s.cam;
cfg.size = ImVec2(-1.0f, 480.0f);
cfg.color = IM_COL32(160, 200, 255, 255);
cfg.wireframe = s.wireframe;
fn::viz::mesh_viewer("##gallery_mesh_viewer", cfg);
}
} // namespace gallery
@@ -1,208 +0,0 @@
// demos_scientific.cpp — demos para los 5 charts cientificos del issue 0034:
// treemap, sankey, chord, contour, voronoi.
#include "demos.h"
#include "demo.h"
#include "viz/treemap.h"
#include "viz/sankey.h"
#include "viz/chord.h"
#include "viz/contour.h"
#include "viz/voronoi.h"
#include <imgui.h>
#include <cmath>
#include <cstdlib>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// treemap
// ---------------------------------------------------------------------------
void demo_treemap() {
demo_header("treemap", "v1.0.0",
"Squarified treemap (Bruls et al.) para jerarquias planas con valores. "
"Algoritmo puro separado del render.");
section("Gastos por categoria");
{
std::vector<TreemapItem> items = {
{"vivienda", 950.0f, IM_COL32(180, 120, 200, 255)},
{"comida", 320.0f, IM_COL32(120, 180, 200, 255)},
{"transporte", 180.0f, IM_COL32(200, 180, 120, 255)},
{"ocio", 140.0f, IM_COL32(200, 120, 160, 255)},
{"salud", 90.0f, IM_COL32(120, 200, 160, 255)},
{"otros", 60.0f, IM_COL32(160, 160, 200, 255)},
};
treemap("##gastos", items, ImVec2(-1, 320));
}
code_block(
"std::vector<TreemapItem> items = {\n"
" {\"vivienda\", 950.0f, IM_COL32(180,120,200,255)},\n"
" {\"comida\", 320.0f, IM_COL32(120,180,200,255)},\n"
" ...\n"
"};\n"
"treemap(\"##gastos\", items, ImVec2(-1, 320));"
);
}
// ---------------------------------------------------------------------------
// sankey
// ---------------------------------------------------------------------------
void demo_sankey() {
demo_header("sankey", "v1.0.0",
"Sankey diagram para flujos source -> target. BFS topologico para columnas, "
"bandas curvas (bezier cubico) para los links. Asume DAG.");
section("Clientes -> productos -> categorias");
{
std::vector<SankeyNode> nodes = {
{"premium"}, {"basicos"},
{"laptops"}, {"phones"}, {"tablets"},
{"hardware"}, {"software"}, {"servicios"},
};
std::vector<SankeyLink> links = {
// clientes -> productos
{0, 2, 80}, {0, 3, 30}, {0, 4, 15},
{1, 3, 60}, {1, 4, 40}, {1, 2, 20},
// productos -> categorias
{2, 5, 70}, {2, 6, 30},
{3, 5, 50}, {3, 7, 40},
{4, 6, 35}, {4, 7, 20},
};
sankey("##flow", nodes, links, ImVec2(-1, 360));
}
code_block(
"std::vector<SankeyNode> nodes = {{\"premium\"}, {\"basicos\"}, ...};\n"
"std::vector<SankeyLink> links = {{0, 2, 80}, {0, 3, 30}, ...};\n"
"sankey(\"##flow\", nodes, links, ImVec2(-1, 360));"
);
}
// ---------------------------------------------------------------------------
// chord
// ---------------------------------------------------------------------------
void demo_chord() {
demo_header("chord", "v1.0.0",
"Chord diagram para matrices NxN. Arcos proporcionales a sum(row) + cuerdas "
"internas con bezier cubico.");
section("Flujos entre paises (matriz 6x6 simetrica)");
{
constexpr int N = 6;
// simetrica de "comercio" entre 6 paises
static float M[N * N] = {
0, 10, 6, 12, 4, 3,
10, 0, 14, 3, 8, 2,
6, 14, 0, 9, 11, 5,
12, 3, 9, 0, 7, 6,
4, 8, 11, 7, 0, 13,
3, 2, 5, 6, 13, 0,
};
static const char* labels[N] = {"ESP", "FRA", "ITA", "DEU", "PRT", "GBR"};
chord("##chord", M, N, labels, ImVec2(420, 420));
}
code_block(
"float M[N*N] = { // simetrica\n"
" 0, 10, 6, 12, 4, 3,\n"
" 10, 0, 14, 3, 8, 2,\n"
" ...\n"
"};\n"
"const char* labels[6] = {\"ESP\",\"FRA\",\"ITA\",\"DEU\",\"PRT\",\"GBR\"};\n"
"chord(\"##c\", M, 6, labels);"
);
}
// ---------------------------------------------------------------------------
// contour
// ---------------------------------------------------------------------------
void demo_contour() {
demo_header("contour", "v1.0.0",
"Contour plot 2D via marching squares. Para una gaussiana centrada los "
"contornos resultantes son aproximadamente concentricos.");
constexpr int N = 32;
static float grid[N * N];
static bool init = false;
if (!init) {
// Mezcla de 2 gaussianas (peak central + secundario)
for (int y = 0; y < N; y++) {
for (int x = 0; x < N; x++) {
float dx1 = x - N * 0.45f, dy1 = y - N * 0.5f;
float dx2 = x - N * 0.75f, dy2 = y - N * 0.3f;
float v = std::exp(-(dx1 * dx1 + dy1 * dy1) / 70.0f)
+ 0.55f * std::exp(-(dx2 * dx2 + dy2 * dy2) / 30.0f);
grid[y * N + x] = v;
}
}
init = true;
}
static const float levels[] = {0.15f, 0.30f, 0.50f, 0.70f, 0.90f};
contour("##gauss", grid, N, N, levels, 5, ImVec2(-1, 320));
code_block(
"constexpr int N = 32;\n"
"float grid[N*N];\n"
"for (int y = 0; y < N; y++)\n"
" for (int x = 0; x < N; x++) {\n"
" float dx = x - N/2.0f, dy = y - N/2.0f;\n"
" grid[y*N + x] = std::exp(-(dx*dx + dy*dy) / 80.0f);\n"
" }\n"
"float levels[] = {0.15f, 0.30f, 0.50f, 0.70f, 0.90f};\n"
"contour(\"##gauss\", grid, N, N, levels, 5);"
);
}
// ---------------------------------------------------------------------------
// voronoi
// ---------------------------------------------------------------------------
void demo_voronoi() {
demo_header("voronoi", "v1.0.0",
"Diagrama de Voronoi via raster brute-force (MVP). Tiles 4x4 px coloreados "
"por el seed mas cercano. Suficiente para N <= 200.");
constexpr int N = 30;
static ImVec2 seeds [N];
static ImU32 colors[N];
static bool init = false;
if (!init) {
unsigned seed = 7;
auto rnd = [&]() {
seed = seed * 1103515245u + 12345u;
return (float)((seed >> 16) & 0x7fff) / 32768.0f;
};
// El render escala automaticamente; las posiciones se asumen en coords del rect.
// Como no sabemos W/H aqui, usamos coords aproximadas para 600x300 y el clip
// dentro de voronoi se encarga de mantenerlas en rango.
for (int i = 0; i < N; i++) {
seeds [i] = ImVec2(rnd() * 600.0f, rnd() * 300.0f);
colors[i] = IM_COL32(40 + (int)(rnd() * 200),
40 + (int)(rnd() * 200),
60 + (int)(rnd() * 195),
230);
}
init = true;
}
voronoi("##v", seeds, N, colors, ImVec2(-1, 300));
code_block(
"ImVec2 seeds[30];\n"
"ImU32 colors[30];\n"
"for (int i = 0; i < 30; i++) {\n"
" seeds [i] = ImVec2(rnd() * 600.0f, rnd() * 300.0f);\n"
" colors[i] = IM_COL32(rnd_byte(), rnd_byte(), rnd_byte(), 230);\n"
"}\n"
"voronoi(\"##v\", seeds, 30, colors, ImVec2(-1, 300));"
);
}
} // namespace gallery
-129
View File
@@ -1,129 +0,0 @@
// Demo de sql_workbench (Core, issue 0032).
//
// Abre `registry.db` en modo readonly y deja que el componente liste sus
// tablas en la sidebar. La idea es probar el ciclo Run + tabla + historial
// contra una DB real sin riesgo de mutarla.
#include "demos.h"
#include "demo.h"
#include "core/sql_workbench.h"
#include "core/tokens.h"
#include <imgui.h>
#include <sqlite3.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
namespace gallery {
namespace {
struct SqlDemoState {
sqlite3* db = nullptr;
fn::SqlWorkbenchState wb;
bool tried_open = false;
std::string db_path;
std::string open_error;
};
SqlDemoState& state() {
static SqlDemoState s;
return s;
}
// Resuelve la ruta a registry.db: env FN_REGISTRY_ROOT/registry.db si existe,
// si no, prueba ./registry.db, ../registry.db, ../../registry.db (build tree).
std::string resolve_registry_db() {
if (const char* env = std::getenv("FN_REGISTRY_ROOT")) {
std::string p = std::string(env) + "/registry.db";
if (FILE* f = std::fopen(p.c_str(), "rb")) { std::fclose(f); return p; }
}
const char* candidates[] = {
"registry.db",
"../registry.db",
"../../registry.db",
"../../../registry.db",
"../../../../registry.db",
};
for (const char* c : candidates) {
if (FILE* f = std::fopen(c, "rb")) { std::fclose(f); return c; }
}
return "";
}
void ensure_open() {
auto& s = state();
if (s.tried_open) return;
s.tried_open = true;
s.db_path = resolve_registry_db();
if (s.db_path.empty()) {
s.open_error = "registry.db not found (tried FN_REGISTRY_ROOT and parent dirs)";
return;
}
int rc = sqlite3_open_v2(s.db_path.c_str(), &s.db,
SQLITE_OPEN_READONLY, nullptr);
if (rc != SQLITE_OK) {
s.open_error = sqlite3_errmsg(s.db);
if (s.db) { sqlite3_close(s.db); s.db = nullptr; }
return;
}
s.wb.readonly = true;
// Query inicial mas util para el demo: lista de funciones del registry.
s.wb.query =
"SELECT id, kind, purity, domain\n"
"FROM functions\n"
"ORDER BY id\n"
"LIMIT 50;";
}
} // namespace
void demo_sql_workbench() {
using namespace fn_tokens;
demo_header("sql_workbench", "v1.0.0",
"Workbench SQL: editor con highlighting, schema sidebar, tabla de "
"resultados e historial. Ejecuta queries contra una sqlite3* del caller. "
"En este demo, registry.db abierto en modo readonly.");
ensure_open();
auto& s = state();
if (!s.open_error.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::error);
ImGui::TextWrapped("could not open registry.db: %s", s.open_error.c_str());
ImGui::PopStyleColor();
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
ImGui::TextWrapped("Set FN_REGISTRY_ROOT to the repo root or run from the repo cwd.");
ImGui::PopStyleColor();
return;
}
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::Text("db: %s (readonly)", s.db_path.c_str());
ImGui::PopStyleColor();
section("workbench");
{
ImVec2 avail = ImGui::GetContentRegionAvail();
// Reserva un pelin para el code_block de abajo.
float h = avail.y - 110.0f;
if (h < 320.0f) h = 320.0f;
fn::sql_workbench("##gallery_sql", s.db, s.wb, ImVec2(-1, h));
}
code_block(
"sqlite3* db = nullptr;\n"
"sqlite3_open_v2(\"registry.db\", &db, SQLITE_OPEN_READONLY, nullptr);\n"
"fn::SqlWorkbenchState st;\n"
"st.readonly = true;\n"
"fn::sql_workbench(\"##sql\", db, st, ImVec2(-1, -1));"
);
}
} // namespace gallery
@@ -1,279 +0,0 @@
// Demos individuales de text_editor y file_watcher (Wave 1, issue 0025).
//
// Aunque las dos primitivas estan diseñadas para componerse, en gallery se
// muestran por separado para que cada entry exhiba un solo primitivo y su
// API minima.
#include "demos.h"
#include "demo.h"
#include "core/text_editor.h"
#include "core/file_watcher.h"
#include "core/button.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <deque>
#include <string>
namespace gallery {
// ===========================================================================
// text_editor — editor de codigo con syntax highlighting
// ===========================================================================
namespace {
const char* kSampleGLSL =
"#version 330\n"
"// fragment shader demo\n"
"out vec4 frag_color;\n"
"uniform vec2 u_resolution;\n"
"uniform float u_time;\n"
"\n"
"void main() {\n"
" vec2 uv = gl_FragCoord.xy / u_resolution;\n"
" vec3 col = 0.5 + 0.5 * cos(u_time + uv.xyx + vec3(0,2,4));\n"
" frag_color = vec4(col, 1.0);\n"
"}\n";
const char* kSampleSQL =
"-- fts5 search sobre el registry\n"
"SELECT id, kind, purity, description\n"
"FROM functions\n"
"WHERE id IN (\n"
" SELECT id FROM functions_fts\n"
" WHERE functions_fts MATCH 'name:slic* OR description:slic*'\n"
")\n"
"ORDER BY name\n"
"LIMIT 50;\n";
const char* kSampleCpp =
"#include <imgui.h>\n"
"namespace fn {\n"
" bool button(const char* label, ButtonVariant v) {\n"
" auto& tk = tokens::current();\n"
" ImGui::PushStyleColor(ImGuiCol_Button, tk.bg_for(v));\n"
" bool clicked = ImGui::Button(label);\n"
" ImGui::PopStyleColor();\n"
" return clicked;\n"
" }\n"
"}\n";
struct EditorState {
fn::TextEditorState* ed = nullptr;
fn::CodeLang lang = fn::CodeLang::GLSL;
};
EditorState& editor_state() {
static EditorState s;
return s;
}
void ensure_editor() {
auto& s = editor_state();
if (!s.ed) {
s.ed = fn::text_editor_create(s.lang);
fn::text_editor_set_text(s.ed, kSampleGLSL);
}
}
void apply_language(fn::CodeLang next) {
auto& s = editor_state();
if (next == s.lang) return;
fn::text_editor_destroy(s.ed);
s.ed = fn::text_editor_create(next);
s.lang = next;
switch (next) {
case fn::CodeLang::GLSL: fn::text_editor_set_text(s.ed, kSampleGLSL); break;
case fn::CodeLang::SQL: fn::text_editor_set_text(s.ed, kSampleSQL); break;
case fn::CodeLang::Cpp: fn::text_editor_set_text(s.ed, kSampleCpp); break;
case fn::CodeLang::Generic: fn::text_editor_set_text(s.ed, ""); break;
}
}
} // namespace
void demo_text_editor() {
using namespace fn_tokens;
demo_header("text_editor", "v1.0.0",
"Editor de codigo embebido en ImGui con syntax highlighting (GLSL/SQL/Cpp/Generic). "
"Wrapper PIMPL sobre ImGuiColorTextEdit (MIT). API: create / set_text / get_text / "
"render / is_dirty.");
ensure_editor();
auto& s = editor_state();
section("language");
{
const char* labels[] = {"GLSL", "SQL", "Cpp", "Generic"};
const fn::CodeLang langs[] = {
fn::CodeLang::GLSL, fn::CodeLang::SQL, fn::CodeLang::Cpp, fn::CodeLang::Generic
};
for (int i = 0; i < 4; ++i) {
if (i > 0) ImGui::SameLine();
bool active = (s.lang == langs[i]);
if (active) ImGui::PushStyleColor(ImGuiCol_Button, colors::primary);
if (ImGui::Button(labels[i])) apply_language(langs[i]);
if (active) ImGui::PopStyleColor();
}
}
section("editor");
{
ImVec2 avail = ImGui::GetContentRegionAvail();
float h = avail.y - 60.0f;
if (h < 220.0f) h = 220.0f;
fn::text_editor_render(s.ed, "##fn_text_editor_solo", ImVec2(-1, h));
if (fn::text_editor_is_dirty(s.ed)) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::warning);
ImGui::TextUnformatted("(modified)");
ImGui::PopStyleColor();
ImGui::SameLine();
if (ImGui::Button("clear dirty##te_solo")) fn::text_editor_clear_dirty(s.ed);
} else {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::TextUnformatted("(clean)");
ImGui::PopStyleColor();
}
}
code_block(
"auto* ed = fn::text_editor_create(fn::CodeLang::GLSL);\n"
"fn::text_editor_set_text(ed, src);\n"
"if (fn::text_editor_render(ed, \"##ed\", {600, 400}))\n"
" on_changed(fn::text_editor_get_text(ed));"
);
}
// ===========================================================================
// file_watcher — watcher cross-platform no bloqueante
// ===========================================================================
namespace {
constexpr const char* kWatchPath = "/tmp/fn_demo.glsl";
struct WatcherDemoState {
fn::FileWatcher* fw = nullptr;
bool active = false;
std::string err;
std::deque<std::string> events;
};
WatcherDemoState& watcher_state() {
static WatcherDemoState s;
return s;
}
void ensure_watcher() {
auto& s = watcher_state();
if (!s.fw) {
s.fw = fn::file_watcher_create();
s.active = fn::file_watcher_add(s.fw, kWatchPath);
if (!s.active) s.err = fn::file_watcher_last_error(s.fw);
}
}
const char* kind_label(fn::FileEvent::Kind k) {
switch (k) {
case fn::FileEvent::Modified: return "MODIFIED";
case fn::FileEvent::Created: return "CREATED";
case fn::FileEvent::Deleted: return "DELETED";
}
return "?";
}
void poll_and_log() {
auto& s = watcher_state();
if (!s.fw) return;
auto evs = fn::file_watcher_poll(s.fw);
for (auto& e : evs) {
char buf[512];
std::snprintf(buf, sizeof(buf), "[%s] %s", kind_label(e.kind), e.path.c_str());
s.events.push_back(buf);
}
while (s.events.size() > 200) s.events.pop_front();
}
bool touch_demo_file(std::string& err_out) {
FILE* f = std::fopen(kWatchPath, "a");
if (!f) { err_out = std::strerror(errno); return false; }
std::fprintf(f, "// touch %ld\n", (long)std::time(nullptr));
std::fclose(f);
return true;
}
} // namespace
void demo_file_watcher() {
using namespace fn_tokens;
demo_header("file_watcher", "v1.0.0",
"Watcher de archivos cross-platform no bloqueante. Linux: inotify. Windows: "
"ReadDirectoryChangesW. API: create / add / poll (drain) / destroy. Cap del "
"buffer de eventos: 200.");
ensure_watcher();
poll_and_log();
auto& s = watcher_state();
section("watcher state");
ImGui::Text("path: %s", kWatchPath);
ImGui::Text("active: %s", s.active ? "yes" : "no");
if (!s.err.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::error);
ImGui::TextWrapped("err: %s", s.err.c_str());
ImGui::PopStyleColor();
}
section("trigger events");
{
if (ImGui::Button("touch (append timestamp)")) {
std::string e;
if (!touch_demo_file(e)) s.err = "touch failed: " + e;
else s.err.clear();
// Si el archivo no existia al inicio, reintenta el add.
if (!s.active) {
s.active = fn::file_watcher_add(s.fw, kWatchPath);
if (!s.active) s.err = fn::file_watcher_last_error(s.fw);
}
}
ImGui::SameLine();
if (ImGui::Button("clear events")) s.events.clear();
ImGui::SameLine();
ImGui::TextDisabled("(o desde otro terminal: echo hi >> %s)", kWatchPath);
}
section("event log");
ImGui::Text("captured: %d", (int)s.events.size());
ImGui::BeginChild("##fw_evlog", ImVec2(0, 0), ImGuiChildFlags_Borders);
if (s.events.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
ImGui::TextWrapped("Sin eventos. Pulsa touch o modifica el path desde otro terminal.");
ImGui::PopStyleColor();
} else {
for (auto it = s.events.rbegin(); it != s.events.rend(); ++it) {
ImGui::TextUnformatted(it->c_str());
}
}
ImGui::EndChild();
code_block(
"auto* fw = fn::file_watcher_create();\n"
"fn::file_watcher_add(fw, \"/tmp/foo.glsl\");\n"
"for (auto& e : fn::file_watcher_poll(fw)) {\n"
" handle_event(e.path, e.kind);\n"
"}"
);
}
} // namespace gallery
-211
View File
@@ -1,211 +0,0 @@
#include "demos.h"
#include "demo.h"
#include "viz/bar_chart.h"
#include "viz/pie_chart.h"
#include "viz/line_plot.h"
#include "viz/scatter_plot.h"
#include "viz/histogram.h"
#include "viz/sparkline.h"
#include "core/tokens.h"
#include <imgui.h>
#include <cmath>
#include <vector>
namespace gallery {
// ---------------------------------------------------------------------------
// bar_chart
// ---------------------------------------------------------------------------
void demo_bar_chart() {
demo_header("bar_chart", "v1.2.0",
"Barras verticales con ejes pineados, tooltip al hover y auto-rotacion 45 grados "
"de labels cuando no caben horizontalmente.");
section("Labels que caben horizontalmente");
{
const char* langs[] = {"go", "py", "ts", "sh", "cpp"};
float values[] = {412.0f, 187.0f, 94.0f, 63.0f, 36.0f};
bar_chart("##bar_short", langs, values, 5, 0.67f, 200.0f);
}
section("Labels largos que obligan a rotar");
{
const char* domains[] = {
"core", "infrastructure", "finance", "datascience",
"cybersecurity", "notebook", "browser"
};
float values[] = {412, 187, 94, 63, 42, 38, 22};
bar_chart("##bar_long", domains, values, 7, 0.67f, 240.0f);
}
code_block(
"const char* labels[] = {\"go\",\"py\",\"ts\",\"sh\",\"cpp\"};\n"
"float values[] = {412,187,94,63,36};\n"
"bar_chart(\"##lang\", labels, values, 5); // h=200 default\n"
"bar_chart(\"##lang\", labels, values, 5, 0.8f, 300); // bar_w + altura"
);
}
// ---------------------------------------------------------------------------
// pie_chart
// ---------------------------------------------------------------------------
void demo_pie_chart() {
demo_header("pie_chart", "v1.1.0",
"Pie/donut con aspect 1:1, ejes pineados y tooltip por slice con "
"valor absoluto + porcentaje.");
if (ImGui::BeginTable("##pie_grid", 2, ImGuiTableFlags_SizingStretchSame)) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
{
const char* labels[] = {"Pure", "Impure"};
float values[] = {412.0f, 278.0f};
variant_label("Pie (radius auto)");
pie_chart("##pie_auto", labels, values, 2, 0.0f, 260.0f);
}
ImGui::TableSetColumnIndex(1);
{
const char* labels[] = {"function", "pipeline", "component"};
float values[] = {618.0f, 42.0f, 230.0f};
variant_label("Donut (radius = -0.45)");
pie_chart("##pie_donut", labels, values, 3, -0.45f, 260.0f);
}
ImGui::EndTable();
}
code_block(
"const char* labels[] = {\"Pure\",\"Impure\"};\n"
"float values[] = {412, 278};\n"
"pie_chart(\"##p\", labels, values, 2); // pie auto\n"
"pie_chart(\"##p\", labels, values, 2, -0.45f, 260); // donut"
);
}
// ---------------------------------------------------------------------------
// line_plot
// ---------------------------------------------------------------------------
void demo_line_plot() {
demo_header("line_plot", "v1.1.0",
"Line plot 2D con limites de ejes calculados de min/max y pineados. "
"Sin auto-fit animado, sin pan/zoom.");
constexpr int N = 100;
static float xs[N], ys[N];
static bool init = false;
if (!init) {
for (int i = 0; i < N; i++) {
xs[i] = static_cast<float>(i) * 0.1f;
ys[i] = std::sin(xs[i]) + 0.3f * std::sin(xs[i] * 3.5f);
}
init = true;
}
line_plot("##line", xs, ys, N, 240.0f);
code_block(
"line_plot(\"##series\", xs, ys, count); // h=200 default\n"
"line_plot(\"##series\", xs, ys, count, 300.0f); // custom height"
);
}
// ---------------------------------------------------------------------------
// scatter_plot
// ---------------------------------------------------------------------------
void demo_scatter_plot() {
demo_header("scatter_plot", "v1.1.0",
"Puntos dispersos con ejes pineados (5% headroom). Sin interaccion.");
constexpr int N = 120;
static float xs[N], ys[N];
static bool init = false;
if (!init) {
unsigned seed = 1234;
auto rnd = [&]() {
seed = seed * 1103515245u + 12345u;
return static_cast<float>((seed >> 16) & 0x7fff) / 32768.0f;
};
for (int i = 0; i < N; i++) {
xs[i] = rnd() * 10.0f;
ys[i] = 0.5f * xs[i] + rnd() * 3.0f;
}
init = true;
}
scatter_plot("##sc", xs, ys, N, 240.0f);
code_block(
"scatter_plot(\"##xy\", xs, ys, count, 240.0f);"
);
}
// ---------------------------------------------------------------------------
// histogram
// ---------------------------------------------------------------------------
void demo_histogram() {
demo_header("histogram", "v1.1.0",
"Histograma con bins automaticos (Sturges) o manuales. Usa AutoFit "
"para los bins + Lock para bloquear pan/zoom.");
constexpr int N = 300;
static float vals[N];
static bool init = false;
if (!init) {
unsigned seed = 42;
auto rnd = [&]() {
seed = seed * 1103515245u + 12345u;
return static_cast<float>((seed >> 16) & 0x7fff) / 32768.0f;
};
// Aproximacion de distribucion normal via box-muller simplificado
for (int i = 0; i < N; i++) {
float u1 = rnd() + 1e-6f;
float u2 = rnd();
vals[i] = std::sqrt(-2.0f * std::log(u1))
* std::cos(2.0f * 3.14159f * u2);
}
init = true;
}
histogram("##hist", vals, N, -1, 240.0f);
code_block(
"histogram(\"##h\", values, count); // bins=Sturges\n"
"histogram(\"##h\", values, count, 30, 300.0f); // 30 bins, h=300"
);
}
// ---------------------------------------------------------------------------
// sparkline
// ---------------------------------------------------------------------------
void demo_sparkline() {
demo_header("sparkline", "v1.0.0",
"Mini grafico de lineas inline (rellenado con alpha + linea). "
"Pensado para tablas, KPI cards, headers.");
float up[] = {10, 12, 11, 15, 18, 17, 20};
float down[] = {30, 28, 29, 25, 22, 24, 20};
float flat[] = {10, 10, 10, 10, 10, 10, 10};
ImGui::Text("Trending up "); ImGui::SameLine();
sparkline("##up", up, 7, ImVec4(0.35f, 0.85f, 0.45f, 1.0f), 140.0f, 22.0f);
ImGui::Text("Trending down"); ImGui::SameLine();
sparkline("##down", down, 7, ImVec4(0.90f, 0.30f, 0.30f, 1.0f), 140.0f, 22.0f);
ImGui::Text("Flat "); ImGui::SameLine();
sparkline("##flat", flat, 7, ImVec4(0.55f, 0.55f, 0.55f, 1.0f), 140.0f, 22.0f);
code_block(
"float history[] = {10,12,11,15,18,17,20};\n"
"sparkline(\"##rev\", history, 7, /*color=*/{0.35,0.85,0.45,1}, 140, 22);"
);
}
} // namespace gallery
-230
View File
@@ -1,230 +0,0 @@
// primitives_gallery — catalogo visual interactivo de los primitivos UI
// del registry (cpp/functions/core y cpp/functions/viz).
//
// Sidebar izquierdo con lista de primitivos agrupados por dominio; panel
// derecho renderiza la demo del item seleccionado (+ snippet de codigo).
//
// Rol: smoke test visual + documentacion viva + build gate en CI.
// NO se conecta a sqlite_api ni a ningun backend. Datos sinteticos.
#include "app_base.h"
#include "imgui.h"
#include "core/fullscreen_window.h"
#include "core/tokens.h"
#include "core/page_header.h"
#include "core/toast.h"
#include "core/app_menubar.h"
#include "core/tree_view.h"
#include "demos.h"
#include "demo.h"
#include "capture.h"
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <sys/stat.h>
#include <vector>
struct DemoEntry {
const char* id; // id estable, apto para comparar seleccion
const char* label; // texto en sidebar
const char* category; // "Core" o "Viz"
void (*fn)(); // puntero a la demo_xxx
};
static const DemoEntry k_demos[] = {
// Core
{"button", "button", "Core", &gallery::demo_button},
{"icon_button", "icon_button", "Core", &gallery::demo_icon_button},
{"toolbar", "toolbar", "Core", &gallery::demo_toolbar},
{"modal_dialog", "modal_dialog", "Core", &gallery::demo_modal},
{"text_input", "text_input", "Core", &gallery::demo_text_input},
{"select", "select", "Core", &gallery::demo_select},
{"toast", "toast + inbox", "Core", &gallery::demo_toast},
{"tree_view", "tree_view", "Core", &gallery::demo_tree_view},
{"badge", "badge", "Core", &gallery::demo_badge},
{"empty_state", "empty_state", "Core", &gallery::demo_empty_state},
{"page_header", "page_header", "Core", &gallery::demo_page_header},
{"dashboard_panel", "dashboard_panel", "Core", &gallery::demo_dashboard_panel},
{"kpi_card", "kpi_card", "Core", &gallery::demo_kpi_card},
{"text_editor", "text_editor", "Core", &gallery::demo_text_editor}, // wave 1
{"file_watcher", "file_watcher", "Core", &gallery::demo_file_watcher}, // wave 1
{"process_runner", "process_runner", "Core", &gallery::demo_process_runner},
{"tween", "tween_curves", "Core", &gallery::demo_tween},
{"bezier_editor", "bezier_editor", "Core", &gallery::demo_bezier_editor},
{"timeline", "timeline", "Core", &gallery::demo_timeline},
{"sql_workbench", "sql_workbench", "Core", &gallery::demo_sql_workbench}, // issue 0032
// Viz
{"bar_chart", "bar_chart", "Viz", &gallery::demo_bar_chart},
{"pie_chart", "pie_chart", "Viz", &gallery::demo_pie_chart},
{"line_plot", "line_plot", "Viz", &gallery::demo_line_plot},
{"scatter_plot", "scatter_plot", "Viz", &gallery::demo_scatter_plot},
{"histogram", "histogram", "Viz", &gallery::demo_histogram},
{"sparkline", "sparkline", "Viz", &gallery::demo_sparkline},
{"graph_viewport", "graph_viewport", "Viz", &gallery::demo_graph},
{"graph_styles", "graph_styles", "Viz", &gallery::demo_graph_styles}, // issue 0049f
{"candlestick", "candlestick", "Viz", &gallery::demo_candlestick},
{"gauge", "gauge", "Viz", &gallery::demo_gauge},
{"heatmap", "heatmap", "Viz", &gallery::demo_heatmap},
{"table_view", "table_view", "Viz", &gallery::demo_table_view},
{"surface_plot_3d", "surface_plot_3d", "Viz", &gallery::demo_surface_plot_3d},
{"scatter_3d", "scatter_3d", "Viz", &gallery::demo_scatter_3d},
{"mesh_viewer", "mesh_viewer", "Viz", &gallery::demo_mesh_viewer},
{"treemap", "treemap", "Viz", &gallery::demo_treemap},
{"sankey", "sankey", "Viz", &gallery::demo_sankey},
{"chord", "chord", "Viz", &gallery::demo_chord},
{"contour", "contour", "Viz", &gallery::demo_contour},
{"voronoi", "voronoi", "Viz", &gallery::demo_voronoi},
// Gfx (shaders_lab core)
{"shader_canvas", "shader_canvas", "Gfx", &gallery::demo_shader_canvas},
{"gl_texture", "gl_texture_load", "Gfx", &gallery::demo_gl_texture}, // wave 1
{"gl_info", "gl_info", "Gfx", &gallery::demo_gl_info}, // issue 0049b
};
static constexpr int k_demo_count = sizeof(k_demos) / sizeof(k_demos[0]);
static std::string g_selected_id = "button";
static const DemoEntry* find_demo(const std::string& id) {
for (int i = 0; i < k_demo_count; i++) {
if (id == k_demos[i].id) return &k_demos[i];
}
return &k_demos[0];
}
static void draw_sidebar() {
ImGui::BeginChild("##gallery_sidebar", ImVec2(220, 0),
ImGuiChildFlags_Borders);
// Agrupar por categoria como rama del tree_view (categorias abiertas por
// defecto). Cada demo es una hoja seleccionable.
int i = 0;
while (i < k_demo_count) {
const char* category = k_demos[i].category;
// Default-open la rama la primera vez que se abre el sidebar.
ImGui::SetNextItemOpen(true, ImGuiCond_FirstUseEver);
if (fn_ui::tree_branch_begin(category, category, /*selected=*/false)) {
// Recorrer todas las demos consecutivas con esta misma categoria.
while (i < k_demo_count
&& std::strcmp(k_demos[i].category, category) == 0) {
const auto& d = k_demos[i];
const bool selected = (g_selected_id == d.id);
fn_ui::tree_leaf(d.id, d.label, selected);
if (fn_ui::tree_node_clicked()) {
g_selected_id = d.id;
}
i++;
}
fn_ui::tree_branch_end();
} else {
// Rama colapsada — saltar todos sus items.
while (i < k_demo_count
&& std::strcmp(k_demos[i].category, category) == 0) {
i++;
}
}
}
ImGui::EndChild();
}
static void render() {
// Theme y gl_loader gestionados por fn::run_app (theme=FnDark por defecto,
// init_gl_loader=true en AppConfig). Menubar via run_app.
// auto_dockspace=false porque usamos fullscreen_window que ocupa todo.
fullscreen_window_begin("##gallery");
page_header_begin("Primitives Gallery",
"Visual catalog of fn_registry C++ UI primitives");
page_header_end();
if (ImGui::BeginTable("##layout", 2,
ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingFixedFit)) {
ImGui::TableSetupColumn("sidebar", ImGuiTableColumnFlags_WidthFixed, 220.0f);
ImGui::TableSetupColumn("content", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
draw_sidebar();
ImGui::TableSetColumnIndex(1);
ImGui::BeginChild("##gallery_content", ImVec2(0, 0),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_HorizontalScrollbar);
// Cuando cambia el tamaño de fuente (Settings > Size), el contenido
// del child crece/encoge pero la posicion de scroll en pixeles
// no — efecto: lo visible "se baja". Escalamos scroll_y por el
// ratio de fuentes para mantener la misma linea logica arriba.
{
static float s_prev_font_size = 0.0f;
float cur_font_size = ImGui::GetStyle().FontSizeBase;
if (s_prev_font_size > 0.0f &&
std::fabs(s_prev_font_size - cur_font_size) > 0.01f) {
ImGui::SetScrollY(ImGui::GetScrollY() *
(cur_font_size / s_prev_font_size));
}
s_prev_font_size = cur_font_size;
}
const DemoEntry* d = find_demo(g_selected_id);
if (d && d->fn) d->fn();
ImGui::EndChild();
ImGui::EndTable();
}
fullscreen_window_end();
// Toasts se renderizan encima para que el demo de toast funcione aqui tambien.
fn_ui::toast_render();
}
int main(int argc, char** argv) {
// Capture mode: `primitives_gallery --capture <output_dir>` corre cada
// demo en una ventana GLFW invisible y guarda PNG por demo. Para CI/golden.
for (int i = 1; i < argc; i++) {
if (std::strcmp(argv[i], "--capture") == 0) {
if (i + 1 >= argc) {
std::fprintf(stderr, "--capture requires an output dir argument\n");
return 2;
}
const char* out_dir = argv[i + 1];
// Best-effort mkdir (idempotente). Windows mkdir() solo acepta el path.
#if defined(_WIN32)
mkdir(out_dir);
#else
mkdir(out_dir, 0755);
#endif
std::vector<gallery::CaptureItem> items;
items.reserve(k_demo_count);
for (int j = 0; j < k_demo_count; j++) {
items.push_back({k_demos[j].id, k_demos[j].fn});
}
gallery::CaptureConfig cfg;
cfg.output_dir = out_dir;
cfg.warmup_frames = 3;
cfg.capture_w = 800;
cfg.capture_h = 600;
const bool ok = gallery::run_capture(cfg, items);
return ok ? 0 : 1;
}
}
return fn::run_app(
{.title = "fn_registry · Primitives Gallery",
.width = 1400,
.height = 900,
.viewports = true,
.about = {.name = "Primitives Gallery",
.version = "0.4.0",
.description = "Visual catalog of fn_registry C++ UI primitives. Now on OpenGL 4.3 core (compute, SSBOs, image load/store) — ver demo gl_info."},
.init_gl_loader = true,
.auto_dockspace = false,
.log = {"primitives_gallery.log", 1}},
render
);
}
@@ -1,47 +0,0 @@
# Tables playground (cpp_apps.md / playgrounds.md). NO se indexa.
# Build flag FN_TQL_DUCKDB=ON activa el adapter tql_duckdb (issue 0080).
option(FN_TQL_DUCKDB "Enable DuckDB SQL execution adapter for tables playground" OFF)
set(_TABLES_SRC
main.cpp
data_table.cpp
data_table_logic.cpp
llm_anthropic.cpp
lua_engine.cpp
tql.cpp
tql_to_sql.cpp
viz.cpp
)
set(_TABLES_TEST_SRC
self_test.cpp
data_table_logic.cpp
llm_anthropic.cpp
lua_engine.cpp
tql.cpp
tql_to_sql.cpp
)
if(FN_TQL_DUCKDB)
list(APPEND _TABLES_SRC tql_duckdb.cpp)
list(APPEND _TABLES_TEST_SRC tql_duckdb.cpp)
endif()
add_imgui_app(tables_playground ${_TABLES_SRC})
target_link_libraries(tables_playground PRIVATE lua54 implot)
if(FN_TQL_DUCKDB)
target_compile_definitions(tables_playground PRIVATE FN_TQL_DUCKDB=1)
target_link_libraries(tables_playground PRIVATE duckdb_vendored)
duckdb_copy_runtime(tables_playground)
endif()
# Self-test E2E (logica pura + lua_engine + tql).
add_executable(tables_playground_self_test ${_TABLES_TEST_SRC})
target_include_directories(tables_playground_self_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/functions
)
target_link_libraries(tables_playground_self_test PRIVATE lua54)
if(FN_TQL_DUCKDB)
target_compile_definitions(tables_playground_self_test PRIVATE FN_TQL_DUCKDB=1)
target_link_libraries(tables_playground_self_test PRIVATE duckdb_vendored)
duckdb_copy_runtime(tables_playground_self_test)
endif()
File diff suppressed because it is too large Load Diff
@@ -1,18 +0,0 @@
#pragma once
#include "data_table_logic.h"
namespace data_table {
// Render barra-de-chips + tabla. Mutates `st` en respuesta a interaccion.
// `declared_types` opcional: array paralelo a headers con ColumnType por col.
// Si nullptr o ColumnType::Auto -> resuelve via auto_detect_type.
// API unificada: `tables` lista todas las tablas disponibles. La que actua como
// main la elige State.main_source (vacio -> tables[0]). El resto se exponen
// como joinables en la UI cuando size > 1.
void render(const char* id,
const std::vector<TableInput>& tables,
State& st,
bool show_chrome = true);
} // namespace data_table
File diff suppressed because it is too large Load Diff
@@ -1,206 +0,0 @@
// Logica pura del playground data_table. Sin ImGui — testable headless.
// TIPOS promovidos al registry (issue 0081). Este header solo declara
// funciones; los types vienen de cpp/functions/core/data_table_types.h.
#pragma once
#include "core/data_table_types.h"
#include <string>
#include <utility>
#include <vector>
namespace data_table {
// ----------------------------------------------------------------------------
// Helpers para Op y ColumnType.
// ----------------------------------------------------------------------------
const char* op_label(Op o);
bool op_is_string_only(Op o);
const char* column_type_name(ColumnType t);
const char* column_type_icon(ColumnType t); // UTF-8 Tabler icon
// Ops permitidos para cada tipo. Devuelve vector ordenado.
std::vector<Op> ops_for_type(ColumnType t);
// Auto-detect via sample: escanea hasta `sample_n` celdas no-vacias.
ColumnType auto_detect_type(const char* const* cells, int rows, int cols,
int col, int sample_n = 64);
// Tipo efectivo: si declared != Auto -> declared; else auto_detect.
ColumnType effective_type(ColumnType declared,
const char* const* cells, int rows, int cols, int col);
// ----------------------------------------------------------------------------
// Aggregation helpers.
// ----------------------------------------------------------------------------
const char* agg_fn_name(AggFn f);
// Pure: alias por defecto cuando agg.alias esta vacio.
// count -> "count"
// distinct col -> "distinct_<col>"
// percentile p -> "p<arg*100>_<col>" (ej. p95_size_kb)
// resto -> "<fn>_<col>" (ej. avg_size_kb)
std::string aggregation_alias(const Aggregation& a);
// Pure: tipo del output de la aggregation.
ColumnType aggregation_type(const Aggregation& a,
const std::vector<std::string>& in_headers,
const std::vector<ColumnType>& in_types);
// ----------------------------------------------------------------------------
// Compute pipeline.
// ----------------------------------------------------------------------------
// Pure: ejecuta un Stage sobre los cells de entrada. Aplica filter -> (group+agg|passthrough) -> sort.
StageOutput compute_stage(const char* const* in_cells, int in_rows, int in_cols,
const std::vector<std::string>& in_headers,
const std::vector<ColumnType>& in_types,
const Stage& stage);
// Pure: aplica filtros usando headers para resolver f.col.
std::vector<int> apply_filters(const char* const* cells, int rows, int cols,
const std::vector<Filter>& filters);
// Pure: helper para drill-down. Devuelve un Filter Op::Eq sobre col_idx con
// el value indicado.
Filter make_drill_filter(int col_idx, const std::string& value);
// ----------------------------------------------------------------------------
// ViewMode helpers.
// ----------------------------------------------------------------------------
const char* view_mode_token(ViewMode m);
const char* view_mode_label(ViewMode m);
ViewMode view_mode_from_token(const char* s);
int view_mode_min_cols(ViewMode m);
bool view_mode_needs_numeric(ViewMode m);
bool view_mode_needs_category(ViewMode m);
bool view_mode_needs_aggregation(ViewMode m);
// Lista completa de modos para el selector UI.
const ViewMode* all_view_modes(int* n_out);
// ----------------------------------------------------------------------------
// Joins (MBQL-style). Ver issue 0078.
// ----------------------------------------------------------------------------
const char* join_strategy_token(JoinStrategy s);
JoinStrategy join_strategy_from_token(const char* s);
const char* join_strategy_label(JoinStrategy s);
// Pure: resuelve indice del main entre `tables` segun `main_source`.
int resolve_main_idx(const std::vector<TableInput>& tables, const std::string& main_source);
// Pure: aplica un join sobre dos tablas.
StageOutput join_tables(const char* const* left_cells, int left_rows, int left_cols,
const std::vector<std::string>& left_headers,
const std::vector<ColumnType>& left_types,
const TableInput& right,
const Join& jn);
// ----------------------------------------------------------------------------
// Drill apply/undo (fase 10).
// ----------------------------------------------------------------------------
bool apply_drill_step(State& st, const DrillStep& step);
bool undo_drill_step(State& st, const DrillStep& step);
// Pure (fase 10): drill-up. Decrementa active_stage si > 0.
bool drill_up(State& st);
// Pure (fase 10): serializa una fila a TSV.
std::string row_to_tsv(const char* const* cells, int rows, int cols,
int row_idx, const std::vector<std::string>& headers);
// Pure (fase 10): construye filters Op::Eq desde una fila.
std::vector<Filter> build_filters_from_row(const char* const* cells, int rows,
int cols, int row_idx);
// ----------------------------------------------------------------------------
// Date granularity helpers (fase 10).
// ----------------------------------------------------------------------------
const char* date_granularity_token(DateGranularity g);
DateGranularity date_granularity_from_token(const char* s);
DateGranularity parse_breakout_granularity(const std::string& breakout,
std::string& col_out);
std::string compose_breakout(const std::string& col, DateGranularity g);
void column_min_max(const char* const* cells, int rows, int cols, int col_idx,
std::string& min_out, std::string& max_out);
// Hit-tests para click-to-drill sobre charts (fase 10).
int nearest_index_1d(double target, const double* xs, int n);
int nearest_index_2d(double tx, double ty,
const double* xs, const double* ys, int n);
double pie_angle(double cx, double cy, double mx, double my);
int pie_slice_at_angle(double angle, const double* sums, int n);
void heatmap_cell_at(double px, double py, int rows, int cols,
int& row_out, int& col_out);
// Date trunc + auto + presets.
std::string truncate_date(const std::string& date, DateGranularity g);
DateGranularity auto_date_granularity(const std::string& min_ymd,
const std::string& max_ymd);
const char* filter_preset_label(FilterPreset p);
std::vector<Filter> build_preset_filters(FilterPreset preset, int col,
const std::string& today_ymd);
// ----------------------------------------------------------------------------
// Misc helpers.
// ----------------------------------------------------------------------------
bool parse_number(const char* s, double& out);
bool compare(const char* a, const char* b, Op op);
std::vector<int> compute_visible_rows(const char* const* cells,
int rows, int cols,
const State& st);
void reorder_column(State& st, int src, int dst);
int find_open_bracket(const char* buf, int len, int cursor, std::string& filter_text);
std::string insert_column_ref(const std::string& src, int start, int cursor,
const std::string& name, int& new_cursor);
std::string csv_escape(const char* s);
std::string build_tsv(const char* const* cells, int rows, int cols,
const char* const* headers,
const std::vector<int>& col_order,
const std::vector<bool>& col_visible,
const std::vector<int>& visible_rows,
int view_row_lo, int view_row_hi,
int view_col_lo, int view_col_hi);
std::string build_csv(const char* const* cells, int rows, int cols,
const char* const* headers,
const std::vector<int>& col_order,
const std::vector<bool>& col_visible,
const std::vector<int>& visible_rows);
// ----------------------------------------------------------------------------
// Column statistics (no movido todavia al registry).
// ----------------------------------------------------------------------------
struct ColStats {
int total = 0;
int empty_count = 0;
int unique_count = 0;
bool unique_capped = false;
bool numeric = false;
int numeric_count = 0;
double min = 0;
double max = 0;
double sum = 0;
double mean = 0;
double p25 = 0;
double p50 = 0;
double p75 = 0;
std::vector<float> hist;
std::vector<std::pair<std::string,int>> top_categories;
};
constexpr int HIST_BINS = 24;
ColStats compute_column_stats(const char* const* cells, int rows, int cols,
int col, int unique_cap = 100000,
const int* indices = nullptr, int n_indices = 0);
} // namespace data_table
@@ -1,29 +0,0 @@
#!/usr/bin/env bash
# E2E playground tables. Compila + corre self-test linux + windows (si
# mingw esta disponible). Sale 0 si todo pasa.
set -euo pipefail
ROOT="${FN_REGISTRY_ROOT:-$(cd "$(dirname "$0")/../../../../.." && pwd)}"
cd "$ROOT"
echo "[e2e] linux build + self_test"
cmake -B cpp/build -S cpp >/dev/null
cmake --build cpp/build --target tables_playground_self_test -j"$(nproc)" >/dev/null
./cpp/build/apps/primitives_gallery/playground/tables/tables_playground_self_test
if command -v x86_64-w64-mingw32-g++ >/dev/null 2>&1; then
echo "[e2e] windows cross-build (mingw)"
source "$ROOT/bash/functions/infra/build_cpp_windows.sh"
build_cpp_windows tables_playground_self_test >/dev/null
echo "[e2e] windows self_test via wine si disponible"
EXE="$ROOT/cpp/build/windows/apps/primitives_gallery/playground/tables/tables_playground_self_test.exe"
if [ -f "$EXE" ]; then
if command -v wine >/dev/null 2>&1; then
wine "$EXE" || { echo "[e2e] FAIL windows self_test (wine)"; exit 1; }
else
echo "[e2e] SKIP wine no instalado; binario en $EXE"
fi
fi
fi
echo "[e2e] OK"
@@ -1,58 +0,0 @@
// llm_anthropic: cliente HTTP minimal a Anthropic Claude API.
// Sin deps externas (cURL via popen).
// Ver issue 0080.
#pragma once
#include "data_table_logic.h"
#include "tql_to_sql.h"
#include <string>
#include <vector>
namespace llm_anthropic {
enum class OutputMode { TQL, SQL };
struct AskInput {
std::string question; // pregunta NL
std::string tql_current; // TQL actual (emitido)
std::vector<std::string> col_names; // schema input
std::vector<data_table::ColumnType> col_types;
std::vector<std::string> joinable_names; // tables disponibles para join
OutputMode mode = OutputMode::TQL;
std::string model; // empty -> default
int max_tokens = 8192;
};
struct AskResult {
std::string code; // bloque ```lua o ```sql extraido (sin fences)
std::string raw; // texto completo de la respuesta
std::string error; // non-empty si fallo
int tokens_in = 0;
int tokens_out = 0;
};
// Pure: construye el system prompt y user message JSON-escapado.
// Devuelve el JSON body completo POST al endpoint /v1/messages.
std::string build_request_body(const AskInput& in);
// Pure: extrae primer ```<lang>\n ... \n``` bloque de `raw`. lang = "lua"|"sql".
// Si no encuentra fence, retorna raw stripped.
std::string extract_code_block(const std::string& raw, const std::string& lang);
// Pure: extrae texto del JSON de respuesta Anthropic.
// Busca `"content":[{"type":"text","text":"..."}]` y devuelve el text.
std::string parse_response_text(const std::string& json_body);
// Impure: lanza cURL via popen, posts `body` al endpoint Anthropic /v1/messages,
// retorna response body (JSON crudo). API key leida de:
// 1. parametro `api_key` si non-empty
// 2. env FN_LLM_API_KEY
// 3. `pass anthropic/api-key | head -n1`
// Si FN_LLM_MOCK_RESPONSE env set, retorna su valor (test injection).
std::string call_api(const std::string& body, const std::string& api_key,
std::string& error_out);
// Orchestrator: build prompt + POST + parse. Convenience wrapper.
AskResult ask(const AskInput& in, const std::string& api_key = "");
} // namespace llm_anthropic
@@ -1,61 +0,0 @@
// Lua 5.4 wrapper para formulas de columnas custom del playground tables.
//
// Features:
// - Sandbox medio: io/require/dofile fuera; os reducido a date/time/diff/clock.
// - Builtins fn.* (~20 funciones).
// - Sintaxis [col_name] preprocesada a row["col_name"].
// - Auto-`return` si la formula es expresion suelta sin keyword inicial.
// - Type-aware push: row.x devuelve number si la col es Int/Float, boolean
// si Bool, string en el resto (Date/String/Json). Nil si vacia.
// - UTF-8 ok en nombres de columnas dentro de [].
// - Comentarios y string literals preservados por el preprocesador.
// - Llamadas recursivas: un derived col puede referenciar a otro derived col;
// ciclos cortados con nil.
#pragma once
#include "data_table_logic.h"
#include <string>
#include <unordered_map>
#include <vector>
// Forward declaration del C struct de Lua (definido en lua.h).
struct lua_State;
namespace lua_engine {
struct Engine;
Engine* get();
void shutdown();
int compile(Engine* e, const std::string& body, std::string* err_out);
void release(Engine* e, int id);
struct RowCtx {
const char* const* cells = nullptr;
int orig_cols = 0;
int row = 0;
const std::vector<std::string>* header_names = nullptr;
const std::unordered_map<std::string,int>* name_to_col = nullptr;
// Tipos declarados/auto-detect de las cols originales. nullptr -> heuristica.
const data_table::ColumnType* types_orig = nullptr;
int n_types_orig = 0;
// Derived cols + lookup por nombre (incluye retipo puro y formulas).
const std::vector<data_table::DerivedColumn>* derived = nullptr;
const std::unordered_map<std::string,int>* derived_name_to_idx = nullptr;
};
std::string eval(Engine* e, int id, const RowCtx& ctx, std::string* err_out);
// Helper expuesto para tests: preprocesa `[col]` -> `row["col"]` respetando
// strings y comentarios. Tambien aplica auto-return.
std::string preprocess(const std::string& body);
// Acceso al lua_State subyacente. Uso restringido: tql.cpp parsea chunks
// (return { ... }) y walks tablas. NO usar para nada que rompa el sandbox.
::lua_State* raw_state();
} // namespace lua_engine
@@ -1,191 +0,0 @@
#include "app_base.h"
#include "imgui.h"
#include "core/logger.h"
#include "data_table.h"
#include <cstdio>
#include <cstring>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace {
// ---------------------------------------------------------------------------
// Dataset generador. Filas se generan con valores deterministas en funcion del
// indice (semilla = i). Strings repetidas (lang/domain/purity/tested) usan
// interned literals -> sin coste de memoria por fila.
// ---------------------------------------------------------------------------
struct Dataset {
int rows = 0;
int cols = 10;
std::vector<std::string> backing; // dynamic strings (name, version, deps, size, cov, date)
std::vector<const char*> cells; // row-major pointers
};
const char* const* dataset_cells(const Dataset& d) { return d.cells.data(); }
std::shared_ptr<Dataset> build_dataset(int rows) {
auto d = std::make_shared<Dataset>();
d->rows = rows;
d->cols = 10;
static const char* langs[] = {"go", "py", "cpp", "bash", "ts"};
static const char* domains[] = {"core", "viz", "infra", "finance", "notebook", "shell"};
static const char* puritys[] = {"pure", "impure"};
static const char* bools[] = {"true", "false"};
// Reserve antes de pushear -> punteros .c_str() estables.
d->backing.reserve((size_t)rows * 6 + 16);
d->cells.reserve((size_t)rows * 10);
auto add = [&](const std::string& s) -> const char* {
d->backing.push_back(s);
return d->backing.back().c_str();
};
char buf[40];
for (int i = 0; i < rows; ++i) {
std::snprintf(buf, sizeof(buf), "fn_%07d", i);
const char* name = add(buf);
const char* lang = langs[i % 5];
const char* domain = domains[i % 6];
const char* purity = puritys[i % 2];
std::snprintf(buf, sizeof(buf), "%d", (i % 5) + 1);
const char* vmaj = add(buf);
std::snprintf(buf, sizeof(buf), "%d", i % 7);
const char* deps = add(buf);
std::snprintf(buf, sizeof(buf), "%.2f", ((i * 31) % 10000) / 100.0);
const char* size = add(buf);
std::snprintf(buf, sizeof(buf), "%.1f", (i % 1001) / 10.0);
const char* cov = add(buf);
const char* tst = bools[(i * 3) % 2];
int y = 2024 + (i % 3);
int m = 1 + (i % 12);
int day = 1 + (i % 28);
std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d", y, m, day);
const char* dt = add(buf);
d->cells.push_back(name);
d->cells.push_back(lang);
d->cells.push_back(domain);
d->cells.push_back(purity);
d->cells.push_back(vmaj);
d->cells.push_back(deps);
d->cells.push_back(size);
d->cells.push_back(cov);
d->cells.push_back(tst);
d->cells.push_back(dt);
}
return d;
}
std::shared_ptr<Dataset>& current_dataset() {
static std::shared_ptr<Dataset> ds;
if (!ds) ds = build_dataset(100);
return ds;
}
} // namespace
void render() {
static data_table::State st;
if (ImGui::Begin("Tables Playground - data_table v0.5")) {
ImGui::TextWrapped(
"v0.5: + en chip-row anade filtro a cualquier col. Show stats muestra "
"0/uniq/mean/min/max por header. Clipper virtualiza render -> 1M filas a 60 FPS.");
ImGui::Separator();
ImGui::Text("Dataset size:");
ImGui::SameLine();
const int sizes[] = {100, 10000, 100000, 1000000};
const char* labels[] = {"100", "10K", "100K", "1M"};
for (size_t i = 0; i < sizeof(sizes)/sizeof(sizes[0]); ++i) {
if (i > 0) ImGui::SameLine();
bool is_active = (current_dataset()->rows == sizes[i]);
if (is_active) ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(80, 120, 80, 255));
if (ImGui::SmallButton(labels[i])) {
current_dataset() = build_dataset(sizes[i]);
st = data_table::State{}; // reset filtros/sort/orden al cambiar dataset
}
if (is_active) ImGui::PopStyleColor();
}
ImGui::SameLine();
ImGui::TextDisabled("(actual: %d filas)", current_dataset()->rows);
ImGui::Separator();
static const char* headers[] = {
"name", "lang", "domain", "purity",
"version_major", "deps_count", "size_kb", "coverage_pct",
"tested", "updated_at"
};
static const data_table::ColumnType types[] = {
data_table::ColumnType::String, // name
data_table::ColumnType::String, // lang
data_table::ColumnType::String, // domain
data_table::ColumnType::String, // purity
data_table::ColumnType::Int, // version_major
data_table::ColumnType::Int, // deps_count
data_table::ColumnType::Float, // size_kb
data_table::ColumnType::Float, // coverage_pct
data_table::ColumnType::Bool, // tested
data_table::ColumnType::Date, // updated_at
};
// Tabla extra para demo de joins (fase 9).
static const char* lang_info_cells[] = {
"go", "compiled", "2009",
"py", "interp", "1991",
"rust", "compiled", "2010",
"ts", "interp", "2012",
"bash", "shell", "1989",
"lua", "interp", "1993",
};
static data_table::TableInput lang_info;
if (lang_info.name.empty()) {
lang_info.name = "lang_info";
lang_info.headers = {"lang", "family", "year"};
lang_info.types = {data_table::ColumnType::String,
data_table::ColumnType::String,
data_table::ColumnType::Int};
lang_info.cells = lang_info_cells;
lang_info.rows = 6;
lang_info.cols = 3;
}
const auto& d = *current_dataset();
static data_table::TableInput main_t;
main_t.name = "fn_registry";
main_t.headers = {"name", "lang", "domain", "purity",
"version_major", "deps_count", "size_kb", "coverage_pct",
"tested", "updated_at"};
main_t.types = std::vector<data_table::ColumnType>(types, types + 10);
main_t.cells = dataset_cells(d);
main_t.rows = d.rows;
main_t.cols = d.cols;
std::vector<data_table::TableInput> tables = { main_t, lang_info };
data_table::render("##bigdata", tables, st);
}
ImGui::End();
}
#ifndef FN_TEST_BUILD
int main() {
return fn::run_app({
.title = "Tables Playground",
.width = 1400,
.height = 900,
.about = {.name = "tables_playground",
.version = "0.5.0",
.description = "Playground data_table: + add filter, stats por columna, "
"clipper para datasets de millones."},
.log = {.file_path = "tables_playground.log",
.level = static_cast<int>(fn_log::Level::Info)}
}, render);
}
#endif
File diff suppressed because it is too large Load Diff
@@ -1,913 +0,0 @@
#include "tql.h"
#include "lua_engine.h"
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <unordered_map>
namespace tql {
using namespace data_table;
namespace {
int find_orig_col(const std::vector<std::string>& headers, const std::string& name) {
for (size_t i = 0; i < headers.size(); ++i) if (headers[i] == name) return (int)i;
return -1;
}
int find_derived_idx(const std::vector<DerivedColumn>& d, const std::string& name) {
for (size_t i = 0; i < d.size(); ++i) if (d[i].name == name) return (int)i;
return -1;
}
Op parse_op(const std::string& s) {
if (s == "=") return Op::Eq;
if (s == "!=") return Op::Neq;
if (s == ">") return Op::Gt;
if (s == ">=") return Op::Gte;
if (s == "<") return Op::Lt;
if (s == "<=") return Op::Lte;
if (s == "contains") return Op::Contains;
if (s == "!contains") return Op::NotContains;
if (s == "starts") return Op::StartsWith;
if (s == "ends") return Op::EndsWith;
return Op::Eq;
}
std::string lua_to_string(lua_State* L, int idx) {
if (lua_isnil(L, idx)) return "";
if (lua_isboolean(L, idx)) return lua_toboolean(L, idx) ? "true" : "false";
size_t n = 0;
const char* s = luaL_tolstring(L, idx, &n);
std::string out(s, n);
lua_pop(L, 1);
return out;
}
} // anon
std::string lua_string_literal(const std::string& s) {
std::string out;
out.reserve(s.size() + 4);
out += '"';
for (char c : s) {
switch (c) {
case '\\': out += "\\\\"; break;
case '"': out += "\\\""; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
default:
if ((unsigned char)c < 0x20) {
char b[8]; std::snprintf(b, sizeof(b), "\\%d", (unsigned char)c);
out += b;
} else out += c;
}
}
out += '"';
return out;
}
std::string color_to_hex(unsigned int c) {
unsigned int r = c & 0xFF;
unsigned int g = (c >> 8) & 0xFF;
unsigned int b = (c >> 16) & 0xFF;
unsigned int a = (c >> 24) & 0xFF;
char buf[16];
if (a == 0xFF) std::snprintf(buf, sizeof(buf), "#%02x%02x%02x", r, g, b);
else std::snprintf(buf, sizeof(buf), "#%02x%02x%02x%02x", r, g, b, a);
return buf;
}
unsigned int hex_to_color(const std::string& s) {
if (s.size() < 7 || s[0] != '#') return 0xFFFFFFFF;
auto hex2 = [&](size_t i) -> unsigned int {
unsigned int v = 0;
if (i + 1 < s.size()) std::sscanf(s.c_str() + i, "%2x", &v);
return v;
};
unsigned int r = hex2(1), g = hex2(3), b = hex2(5);
unsigned int a = (s.size() >= 9) ? hex2(7) : 0xFF;
return r | (g << 8) | (b << 16) | (a << 24);
}
ColumnType column_type_from_string(const std::string& s) {
if (s == "string") return ColumnType::String;
if (s == "int") return ColumnType::Int;
if (s == "float") return ColumnType::Float;
if (s == "bool") return ColumnType::Bool;
if (s == "date") return ColumnType::Date;
if (s == "json") return ColumnType::Json;
return ColumnType::Auto;
}
// Helper: header del Stage 0 dado un col idx eff. Para stages 1+ no aplica
// (los stage outputs tienen sus propios headers).
namespace {
const char* agg_fn_token(AggFn f) {
switch (f) {
case AggFn::Count: return "count";
case AggFn::Sum: return "sum";
case AggFn::Avg: return "avg";
case AggFn::Min: return "min";
case AggFn::Max: return "max";
case AggFn::Distinct: return "distinct";
case AggFn::Stddev: return "stddev";
case AggFn::Median: return "median";
case AggFn::P25: return "p25";
case AggFn::P75: return "p75";
case AggFn::P90: return "p90";
case AggFn::P99: return "p99";
case AggFn::Percentile: return "percentile";
}
return "?";
}
AggFn agg_fn_from_string(const std::string& s) {
if (s == "count") return AggFn::Count;
if (s == "sum") return AggFn::Sum;
if (s == "avg") return AggFn::Avg;
if (s == "min") return AggFn::Min;
if (s == "max") return AggFn::Max;
if (s == "distinct") return AggFn::Distinct;
if (s == "stddev") return AggFn::Stddev;
if (s == "median") return AggFn::Median;
if (s == "p25") return AggFn::P25;
if (s == "p75") return AggFn::P75;
if (s == "p90") return AggFn::P90;
if (s == "p99") return AggFn::P99;
if (s == "percentile") return AggFn::Percentile;
return AggFn::Count;
}
} // anon
std::string emit(const State& state,
const std::vector<std::string>& headers,
const std::vector<ColumnType>& types)
{
int orig_cols = (int)headers.size();
const Stage& raw = state.raw();
int eff_cols = orig_cols + (int)raw.derived.size();
// Build effective headers + types (same indexing as col_visible/order)
std::vector<std::string> eff_headers(eff_cols);
std::vector<ColumnType> eff_types(eff_cols);
for (int c = 0; c < orig_cols; ++c) {
eff_headers[c] = headers[c];
eff_types[c] = (c < (int)types.size()) ? types[c] : ColumnType::Auto;
}
for (int k = 0; k < (int)raw.derived.size(); ++k) {
eff_headers[orig_cols + k] = raw.derived[k].name;
eff_types[orig_cols + k] = raw.derived[k].type;
}
// Build order positions: col_idx -> visual order (1-based)
std::unordered_map<int, int> order_pos;
for (size_t i = 0; i < state.col_order.size(); ++i) {
order_pos[state.col_order[i]] = (int)i + 1;
}
auto emit_filter_block = [&](const std::vector<Filter>& filters,
const std::vector<std::string>& stage_headers,
const char* indent) -> std::string {
if (filters.empty()) return {};
std::string s;
s += indent; s += "filter = {\n";
for (const auto& f : filters) {
std::string col_name = (f.col >= 0 && f.col < (int)stage_headers.size())
? stage_headers[f.col] : "";
s += indent; s += " {";
s += lua_string_literal(op_label(f.op));
s += ", ";
s += lua_string_literal(col_name);
s += ", ";
s += lua_string_literal(f.value);
s += "},\n";
}
s += indent; s += "},\n";
return s;
};
auto emit_sort_block = [&](const std::vector<SortClause>& sorts,
const char* indent) -> std::string {
if (sorts.empty()) return {};
std::string s;
s += indent; s += "sort = {\n";
for (const auto& sc : sorts) {
s += indent; s += " {";
s += lua_string_literal(sc.desc ? "desc" : "asc");
s += ", ";
s += lua_string_literal(sc.col);
s += "},\n";
}
s += indent; s += "},\n";
return s;
};
std::string out;
out += "-- TQL v1 (Table Query Language). Round-trip de State <-> Lua.\n";
out += "-- Schema:\n";
out += "-- version = 1 -- bump si breaking change\n";
out += "-- display = \"table\" -- table|bar|line|pie (futuro)\n";
out += "-- stages = { stage0, stage1, ... } -- pipeline; stage 0 = Raw\n";
out += "-- columns = { {name,type,visible,order,color_rules}, ... }\n";
out += "--\n";
out += "-- Stage 0 (Raw): filter + expressions + sort\n";
out += "-- Stage N (Grouped): filter + breakout + aggregation + sort\n";
out += "--\n";
out += "-- filter: {{op, col, val}, ...} op in =,!=,>,>=,<,<=,contains,!contains,starts,ends\n";
out += "-- expressions: {[name] = \"lua_body\"} ej: [\"total\"] = \"return [a] + [b]\"\n";
out += "-- breakout: {\"col1\", \"col2\"} group by\n";
out += "-- aggregation: {{fn, col, arg?}, ...} fn in count,sum,avg,min,max,distinct,stddev,median,p25,p75,p90,p99,percentile\n";
out += "-- sort: {{dir, col}, ...} dir in asc,desc\n";
out += "return {\n";
out += " version = 1,\n";
out += " display = ";
out += lua_string_literal(view_mode_token(state.display));
out += ",\n";
if (!state.main_source.empty()) {
out += " main_source = ";
out += lua_string_literal(state.main_source);
out += ",\n";
}
// joins (antes de stages, materializa input)
if (!state.joins.empty()) {
out += " joins = {\n";
for (const auto& jn : state.joins) {
out += " {alias = " + lua_string_literal(jn.alias);
out += ", source = " + lua_string_literal(jn.source);
out += ", strategy = " + lua_string_literal(join_strategy_token(jn.strategy));
out += ", on = {";
for (size_t i = 0; i < jn.on.size(); ++i) {
if (i) out += ", ";
out += "{" + lua_string_literal(jn.on[i].first) + ", "
+ lua_string_literal(jn.on[i].second) + "}";
}
out += "}";
if (!jn.fields.empty()) {
out += ", fields = {";
for (size_t i = 0; i < jn.fields.size(); ++i) {
if (i) out += ", ";
out += lua_string_literal(jn.fields[i]);
}
out += "}";
}
out += "},\n";
}
out += " },\n";
}
out += " stages = {\n";
// Recorre todos los stages; stage 0 tiene formato Raw (filter+expr+sort),
// stages 1+ tienen formato Grouped (filter+breakout+aggregation+sort).
// Headers para resolver col indices de filters/sorts se computan stage por
// stage simulando la cadena.
std::vector<std::string> cur_headers = headers; // stage input headers
// Para stage 0 raw, los headers incluyen orig + derived.
// Construye cur_headers iniciales (= orig); derived se anaden al pasar stage 0.
for (int si = 0; si < (int)state.stages.size(); ++si) {
const Stage& stg = state.stages[si];
out += " {\n";
if (si == 0) {
// Stage 0: orig headers + derived seran disponibles tras expressions.
// Para los filter col indices, asumimos van con cur_headers = orig.
// (data_table.cpp solo aplica filters a orig cols al guardar; si en
// futuro stage 0 admite filter sobre derived, se traduce a name.)
std::vector<std::string> s0_headers = headers;
// Filters
out += emit_filter_block(stg.filters, s0_headers, " ");
// Expressions
if (!stg.derived.empty()) {
bool any = false;
for (const auto& d : stg.derived) if (!d.formula.empty()) { any = true; break; }
if (any) {
out += " expressions = {\n";
for (const auto& d : stg.derived) {
if (d.formula.empty()) continue;
out += " [";
out += lua_string_literal(d.name);
out += "] = ";
out += lua_string_literal(d.formula);
out += ",\n";
}
out += " },\n";
}
}
// Sort (sort.col es string en nuevo modelo).
out += emit_sort_block(stg.sorts, " ");
// Avanza cur_headers para siguiente stage: orig + derived.
for (const auto& d : stg.derived) cur_headers.push_back(d.name);
} else {
// Stage 1+: filter (sobre output del previo, cur_headers).
out += emit_filter_block(stg.filters, cur_headers, " ");
// breakout
if (!stg.breakouts.empty()) {
out += " breakout = {";
for (size_t i = 0; i < stg.breakouts.size(); ++i) {
if (i > 0) out += ", ";
out += lua_string_literal(stg.breakouts[i]);
}
out += "},\n";
}
// aggregation
if (!stg.aggregations.empty()) {
out += " aggregation = {\n";
for (const auto& a : stg.aggregations) {
out += " {";
out += lua_string_literal(agg_fn_token(a.fn));
if (a.fn != AggFn::Count) {
out += ", ";
out += lua_string_literal(a.col);
}
if (a.fn == AggFn::Percentile) {
char buf[32]; std::snprintf(buf, sizeof(buf), "%g", a.arg);
out += ", "; out += buf;
}
out += "},\n";
}
out += " },\n";
}
// sort
out += emit_sort_block(stg.sorts, " ");
// Avanza cur_headers para siguiente stage: breakouts + agg aliases.
std::vector<std::string> next;
for (const auto& b : stg.breakouts) next.push_back(b);
for (const auto& a : stg.aggregations) next.push_back(aggregation_alias(a));
cur_headers = std::move(next);
}
out += " },\n";
}
out += " },\n";
// columns (per-col render config) — siempre referidas a los effective cols
// del STAGE 0 (asumimos viz state para stage 0 / raw). Renderizar columns
// por cada stage no aporta v1.
out += " columns = {\n";
for (int c = 0; c < eff_cols; ++c) {
out += " {";
out += "name = " + lua_string_literal(eff_headers[c]);
out += ", type = " + lua_string_literal(column_type_name(eff_types[c]));
bool vis = (c < (int)state.col_visible.size()) ? state.col_visible[c] : true;
out += std::string(", visible = ") + (vis ? "true" : "false");
int order = order_pos.count(c) ? order_pos[c] : c + 1;
out += ", order = " + std::to_string(order);
// color rules for this col
bool first = true;
for (const auto& cr : state.color_rules) {
if (cr.col != c) continue;
if (first) { out += ", color_rules = {"; first = false; }
else { out += ", "; }
out += "{equals = " + lua_string_literal(cr.equals);
out += ", color = " + lua_string_literal(color_to_hex(cr.color)) + "}";
}
if (!first) out += "}";
out += "},\n";
}
out += " },\n";
// views (extra viz panels — viz adicional sobre mismos stages)
auto emit_view = [&](const VizPanel& p) -> std::string {
std::string s = " {";
s += "display = " + lua_string_literal(view_mode_token(p.display));
if (!p.config.x_col.empty()) s += ", x_col = " + lua_string_literal(p.config.x_col);
if (!p.config.cat_col.empty()) s += ", cat_col = " + lua_string_literal(p.config.cat_col);
if (!p.config.size_col.empty()) s += ", size_col = "+ lua_string_literal(p.config.size_col);
if (!p.config.y_cols.empty()) {
s += ", y_cols = {";
for (size_t i = 0; i < p.config.y_cols.size(); ++i) {
if (i) s += ", ";
s += lua_string_literal(p.config.y_cols[i]);
}
s += "}";
}
if (p.config.primary_color != 0)
s += ", color = " + lua_string_literal(color_to_hex(p.config.primary_color));
if (p.config.hist_bins > 0)
s += ", hist_bins = " + std::to_string(p.config.hist_bins);
if (p.config.pie_radius > 0)
s += ", pie_radius = " + std::to_string(p.config.pie_radius);
if (!p.config.show_legend) s += ", show_legend = false";
if (p.config.show_markers) s += ", show_markers = true";
if (p.config.locked) s += ", locked = true";
s += "},\n";
return s;
};
out += " views = {\n";
// Panel 0 = main viz
VizPanel main_p;
main_p.display = state.display;
main_p.config = state.viz_config;
out += emit_view(main_p);
for (const auto& p : state.extra_panels) out += emit_view(p);
out += " },\n";
out += " visualization_settings = {},\n";
out += "}\n";
return out;
}
bool apply(const std::string& lua_text, State& state,
const std::vector<std::string>& headers,
const std::vector<ColumnType>& /*types*/,
const char* const* cells, int rows, int orig_cols,
std::string* err)
{
std::vector<std::string> warns;
auto warn = [&](const std::string& m) { warns.push_back(m); };
auto finish_with_warns = [&](bool ok) -> bool {
if (err && !warns.empty()) {
std::string j;
for (size_t i = 0; i < warns.size(); ++i) {
if (i) j += "; ";
j += warns[i];
}
*err = j;
}
return ok;
};
lua_State* L = lua_engine::raw_state();
if (!L) { if (err) *err = "lua engine null"; return false; }
if (luaL_loadbufferx(L, lua_text.data(), lua_text.size(), "tql", "t") != LUA_OK) {
if (err) *err = lua_tostring(L, -1) ? lua_tostring(L, -1) : "load error";
lua_pop(L, 1);
return false;
}
if (lua_pcall(L, 0, 1, 0) != LUA_OK) {
if (err) *err = lua_tostring(L, -1) ? lua_tostring(L, -1) : "exec error";
lua_pop(L, 1);
return false;
}
if (!lua_istable(L, -1)) {
if (err) *err = "TQL root must be a table";
lua_pop(L, 1);
return false;
}
// main_source
lua_getfield(L, -1, "main_source");
if (lua_isstring(L, -1)) state.main_source = lua_tostring(L, -1);
else state.main_source.clear();
lua_pop(L, 1);
// display
lua_getfield(L, -1, "display");
if (lua_isstring(L, -1)) {
std::string d = lua_tostring(L, -1);
ViewMode m = view_mode_from_token(d.c_str());
state.display = m;
if (d != "table" && std::strcmp(view_mode_token(m), d.c_str()) != 0) {
warn("unknown display \"" + d + "\" (defaulting to table)");
}
}
lua_pop(L, 1);
// Validar version.
lua_getfield(L, -1, "version");
if (lua_isnil(L, -1)) {
warn("version missing (assuming 1)");
} else if (!lua_isnumber(L, -1)) {
if (err) *err = "version must be a number";
lua_pop(L, 2);
return false;
} else {
int v = (int)lua_tointeger(L, -1);
if (v != 1) {
char buf[64]; std::snprintf(buf, sizeof(buf), "unsupported TQL version %d (expected 1)", v);
if (err) *err = buf;
lua_pop(L, 2);
return false;
}
}
lua_pop(L, 1);
// Reset partes mutables. Liberar lua_ids antes.
for (auto& s : state.stages) {
for (auto& d : s.derived) {
if (d.lua_id >= 0) lua_engine::release(lua_engine::get(), d.lua_id);
}
}
state.stages.clear();
state.active_stage = 0;
state.color_rules.clear();
// ---- Walk joins[] ----
state.joins.clear();
lua_getfield(L, -1, "joins");
if (lua_istable(L, -1)) {
int nj = (int)lua_rawlen(L, -1);
for (int i = 1; i <= nj; ++i) {
lua_rawgeti(L, -1, i);
if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }
Join jn;
lua_getfield(L, -1, "alias");
if (lua_isstring(L, -1)) jn.alias = lua_tostring(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "source");
if (lua_isstring(L, -1)) jn.source = lua_tostring(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "strategy");
if (lua_isstring(L, -1)) jn.strategy = join_strategy_from_token(lua_tostring(L, -1));
lua_pop(L, 1);
lua_getfield(L, -1, "on");
if (lua_istable(L, -1)) {
int on_n = (int)lua_rawlen(L, -1);
for (int k = 1; k <= on_n; ++k) {
lua_rawgeti(L, -1, k);
if (lua_istable(L, -1) && lua_rawlen(L, -1) >= 2) {
lua_rawgeti(L, -1, 1); std::string a = lua_to_string(L, -1); lua_pop(L, 1);
lua_rawgeti(L, -1, 2); std::string b = lua_to_string(L, -1); lua_pop(L, 1);
jn.on.push_back({a, b});
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
lua_getfield(L, -1, "fields");
if (lua_istable(L, -1)) {
int fn_n = (int)lua_rawlen(L, -1);
for (int k = 1; k <= fn_n; ++k) {
lua_rawgeti(L, -1, k);
if (lua_isstring(L, -1)) jn.fields.emplace_back(lua_tostring(L, -1));
lua_pop(L, 1);
}
}
lua_pop(L, 1);
state.joins.push_back(jn);
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// ---- Walk stages[] ----
lua_getfield(L, -1, "stages");
if (lua_istable(L, -1)) {
int n_stages = (int)lua_rawlen(L, -1);
// Headers efectivos por stage para resolver filter/sort col indices.
std::vector<std::string> cur_headers = headers;
for (int si = 1; si <= n_stages; ++si) {
lua_rawgeti(L, -1, si);
if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }
Stage stg;
// Stage 0 expressions (solo aplica a si == 1, pero permitimos en
// cualquier stage por simetria — el UI no las expone en stages 1+).
lua_getfield(L, -1, "expressions");
if (lua_istable(L, -1)) {
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if (lua_isstring(L, -2) && lua_isstring(L, -1)) {
std::string name = lua_tostring(L, -2);
std::string formula = lua_tostring(L, -1);
std::string cerr;
int id = lua_engine::compile(lua_engine::get(), formula, &cerr);
DerivedColumn d;
d.source_col = -1;
d.name = name;
d.formula = formula;
d.lua_id = id;
d.compile_error = (id < 0) ? cerr : "";
if (id >= 0 && si == 1) {
// auto-detect tipo via sample (solo para stage 0).
int sample = std::min(64, rows);
std::vector<std::string> samples_str;
std::vector<const char*> samples_ptr;
std::vector<std::string> hn_storage = headers;
std::unordered_map<std::string, int> n2c;
for (int c = 0; c < orig_cols && c < (int)hn_storage.size(); ++c) {
n2c[hn_storage[c]] = c;
}
for (int r = 0; r < sample; ++r) {
lua_engine::RowCtx ctx;
ctx.cells = cells;
ctx.orig_cols = orig_cols;
ctx.row = r;
ctx.header_names = &hn_storage;
ctx.name_to_col = &n2c;
std::string e;
samples_str.emplace_back(
lua_engine::eval(lua_engine::get(), id, ctx, &e));
}
for (auto& s : samples_str) samples_ptr.push_back(s.c_str());
d.type = auto_detect_type(samples_ptr.data(),
(int)samples_ptr.size(), 1, 0);
} else {
d.type = ColumnType::String;
}
stg.derived.push_back(d);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// filter
lua_getfield(L, -1, "filter");
if (lua_istable(L, -1)) {
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (lua_istable(L, -1) && lua_rawlen(L, -1) >= 3) {
lua_rawgeti(L, -1, 1); std::string op = lua_to_string(L, -1); lua_pop(L, 1);
lua_rawgeti(L, -1, 2); std::string col_name = lua_to_string(L, -1); lua_pop(L, 1);
lua_rawgeti(L, -1, 3); std::string val = lua_to_string(L, -1); lua_pop(L, 1);
int ci = find_orig_col(cur_headers, col_name);
if (ci >= 0) {
stg.filters.push_back({ci, parse_op(op), val});
} else {
warn("stage " + std::to_string(si - 1) + ": filter col \"" + col_name + "\" not found");
}
if (op != "=" && op != "!=" && op != ">" && op != ">=" &&
op != "<" && op != "<=" && op != "contains" &&
op != "!contains" && op != "starts" && op != "ends") {
warn("stage " + std::to_string(si - 1) + ": unknown filter op \"" + op + "\" (defaulting to =)");
}
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// breakout (solo aplica stages >= 1, no-op silencioso si stage 0).
// Acepta sufijo ":granularity" para cols Date (fase 10).
lua_getfield(L, -1, "breakout");
if (lua_istable(L, -1)) {
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (lua_isstring(L, -1)) {
std::string bn = lua_tostring(L, -1);
std::string clean;
parse_breakout_granularity(bn, clean);
if (find_orig_col(cur_headers, clean) < 0) {
warn("stage " + std::to_string(si - 1) + ": breakout col \"" + clean + "\" not in input headers");
}
stg.breakouts.emplace_back(bn);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// aggregation
lua_getfield(L, -1, "aggregation");
if (lua_istable(L, -1)) {
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (lua_istable(L, -1) && lua_rawlen(L, -1) >= 1) {
Aggregation a;
lua_rawgeti(L, -1, 1);
std::string fn_name = lua_to_string(L, -1);
lua_pop(L, 1);
bool known = (fn_name == "count" || fn_name == "sum" || fn_name == "avg" ||
fn_name == "min" || fn_name == "max" || fn_name == "distinct" ||
fn_name == "stddev"|| fn_name == "median" ||
fn_name == "p25" || fn_name == "p75" || fn_name == "p90" ||
fn_name == "p99" || fn_name == "percentile");
if (!known) {
warn("stage " + std::to_string(si - 1) + ": unknown aggregation fn \"" + fn_name + "\" (defaulting to count)");
}
a.fn = agg_fn_from_string(fn_name);
if (lua_rawlen(L, -1) >= 2) {
lua_rawgeti(L, -1, 2);
a.col = lua_to_string(L, -1);
lua_pop(L, 1);
if (a.fn != AggFn::Count && find_orig_col(cur_headers, a.col) < 0) {
warn("stage " + std::to_string(si - 1) + ": aggregation col \"" + a.col + "\" not in input headers");
}
} else if (a.fn != AggFn::Count) {
warn("stage " + std::to_string(si - 1) + ": aggregation \"" + fn_name + "\" requires a column");
}
if (lua_rawlen(L, -1) >= 3) {
lua_rawgeti(L, -1, 3);
if (lua_isnumber(L, -1)) a.arg = lua_tonumber(L, -1);
lua_pop(L, 1);
}
stg.aggregations.push_back(a);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// sort
lua_getfield(L, -1, "sort");
if (lua_istable(L, -1)) {
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (lua_istable(L, -1) && lua_rawlen(L, -1) >= 2) {
lua_rawgeti(L, -1, 1); std::string dir = lua_to_string(L, -1); lua_pop(L, 1);
lua_rawgeti(L, -1, 2); std::string col = lua_to_string(L, -1); lua_pop(L, 1);
SortClause sc;
sc.col = col;
sc.desc = (dir == "desc");
if (dir != "asc" && dir != "desc") {
warn("stage " + std::to_string(si - 1) + ": unknown sort dir \"" + dir + "\" (defaulting to asc)");
}
stg.sorts.push_back(sc);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
state.stages.push_back(std::move(stg));
// Advance cur_headers para resolver filter/sort col del siguiente stage.
const Stage& last = state.stages.back();
if (si == 1) {
// Stage 0: cur_headers = orig + derived (sin breakouts/agg).
for (const auto& d : last.derived) cur_headers.push_back(d.name);
} else {
if (!last.breakouts.empty() || !last.aggregations.empty()) {
std::vector<std::string> next;
for (const auto& b : last.breakouts) next.push_back(b);
for (const auto& a : last.aggregations) next.push_back(aggregation_alias(a));
cur_headers = std::move(next);
}
}
lua_pop(L, 1); // pop stage entry
}
}
lua_pop(L, 1); // stages
state.ensure_stage0();
// ---- Walk columns (per-col render config) ----
int eff_cols = orig_cols + (int)state.raw().derived.size();
lua_getfield(L, -1, "columns");
if (lua_istable(L, -1)) {
state.col_visible.assign(eff_cols, true);
std::vector<std::pair<int,int>> order_pairs;
std::vector<bool> seen(eff_cols, false);
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }
lua_getfield(L, -1, "name");
std::string nm = lua_to_string(L, -1);
lua_pop(L, 1);
int col_idx = find_orig_col(headers, nm);
if (col_idx < 0) {
int di = find_derived_idx(state.raw().derived, nm);
if (di >= 0) col_idx = orig_cols + di;
}
if (col_idx < 0 || col_idx >= eff_cols) { lua_pop(L, 1); continue; }
seen[col_idx] = true;
// visible
lua_getfield(L, -1, "visible");
if (lua_isboolean(L, -1)) state.col_visible[col_idx] = lua_toboolean(L, -1);
lua_pop(L, 1);
// order
lua_getfield(L, -1, "order");
int order_val = lua_isnumber(L, -1) ? (int)lua_tointeger(L, -1) : (col_idx + 1);
lua_pop(L, 1);
order_pairs.emplace_back(order_val, col_idx);
// type (mutable solo para derived)
lua_getfield(L, -1, "type");
if (lua_isstring(L, -1)) {
std::string tn = lua_tostring(L, -1);
ColumnType t = column_type_from_string(tn);
if (col_idx >= orig_cols) {
state.raw().derived[col_idx - orig_cols].type = t;
}
}
lua_pop(L, 1);
// color_rules
lua_getfield(L, -1, "color_rules");
if (lua_istable(L, -1)) {
int rn = (int)lua_rawlen(L, -1);
for (int j = 1; j <= rn; ++j) {
lua_rawgeti(L, -1, j);
if (lua_istable(L, -1)) {
lua_getfield(L, -1, "equals");
std::string eq = lua_to_string(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "color");
std::string hx = lua_to_string(L, -1);
lua_pop(L, 1);
state.color_rules.push_back({col_idx, eq, hex_to_color(hx)});
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
lua_pop(L, 1); // pop entry
}
std::sort(order_pairs.begin(), order_pairs.end());
state.col_order.clear();
for (auto& p : order_pairs) state.col_order.push_back(p.second);
for (int c = 0; c < eff_cols; ++c) if (!seen[c]) state.col_order.push_back(c);
}
lua_pop(L, 1); // columns
// ---- Walk views[] (extra viz panels) ----
state.extra_panels.clear();
lua_getfield(L, -1, "views");
if (lua_istable(L, -1)) {
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }
VizPanel p;
lua_getfield(L, -1, "display");
if (lua_isstring(L, -1)) p.display = view_mode_from_token(lua_tostring(L, -1));
lua_pop(L, 1);
auto read_str = [&](const char* key, std::string& out_s) {
lua_getfield(L, -1, key);
if (lua_isstring(L, -1)) out_s = lua_tostring(L, -1);
lua_pop(L, 1);
};
read_str("x_col", p.config.x_col);
read_str("cat_col", p.config.cat_col);
read_str("size_col", p.config.size_col);
lua_getfield(L, -1, "y_cols");
if (lua_istable(L, -1)) {
int yn = (int)lua_rawlen(L, -1);
for (int j = 1; j <= yn; ++j) {
lua_rawgeti(L, -1, j);
if (lua_isstring(L, -1)) p.config.y_cols.emplace_back(lua_tostring(L, -1));
lua_pop(L, 1);
}
}
lua_pop(L, 1);
lua_getfield(L, -1, "color");
if (lua_isstring(L, -1)) p.config.primary_color = hex_to_color(lua_tostring(L, -1));
lua_pop(L, 1);
lua_getfield(L, -1, "hist_bins");
if (lua_isnumber(L, -1)) p.config.hist_bins = (int)lua_tointeger(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "pie_radius");
if (lua_isnumber(L, -1)) p.config.pie_radius = (float)lua_tonumber(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "show_legend");
if (lua_isboolean(L, -1)) p.config.show_legend = lua_toboolean(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "show_markers");
if (lua_isboolean(L, -1)) p.config.show_markers = lua_toboolean(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "locked");
if (lua_isboolean(L, -1)) p.config.locked = lua_toboolean(L, -1);
lua_pop(L, 1);
// Panel 0 = main viz (state.display + state.viz_config).
if (i == 1) {
state.display = p.display;
state.viz_config = p.config;
} else {
state.extra_panels.push_back(p);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1); // views
lua_pop(L, 1); // pop root
return finish_with_warns(true);
}
} // namespace tql
@@ -1,42 +0,0 @@
// TQL — Table Query Language emit/apply. Round-trip entre State y Lua text.
// Ver docs/TQL.md.
#pragma once
#include "data_table_logic.h"
#include <string>
#include <vector>
namespace tql {
// Serializa el estado actual a un Lua chunk completo:
// return { version, display, stages, columns, visualization_settings }
//
// `headers` y `types` describen las cols originales (size = orig_cols).
// Las derived cols se anaden automaticamente desde state.derived.
std::string emit(const data_table::State& state,
const std::vector<std::string>& headers,
const std::vector<data_table::ColumnType>& types);
// Parsea un Lua chunk TQL y rellena State. Mutates:
// - stages (clears + reconstruye desde stages[] del TQL; stage 0 = Raw con
// filters/expressions/sort; stages 1+ con filter/breakout/aggregation/sort)
// - col_visible / col_order (desde columns[])
// - color_rules (desde columns[].color_rules)
// - stages[0].derived[].type (desde columns[].type para nombres derived)
//
// `cells/rows/orig_cols` necesarios para sample auto-detect de tipos en
// expressions (cuando la entry columns omite el type explicito).
bool apply(const std::string& lua_text,
data_table::State& state,
const std::vector<std::string>& headers,
const std::vector<data_table::ColumnType>& types,
const char* const* cells, int rows, int orig_cols,
std::string* err);
// Helpers expuestos para tests.
std::string lua_string_literal(const std::string& s);
std::string color_to_hex(unsigned int c);
unsigned int hex_to_color(const std::string& s);
data_table::ColumnType column_type_from_string(const std::string& s);
} // namespace tql
@@ -1,231 +0,0 @@
// tql_duckdb.cpp — DuckDB adapter para ejecutar SQL emitido por tql_to_sql.
// Compilado solo si FN_TQL_DUCKDB define. Ver issue 0080.
#include "tql_duckdb.h"
#ifdef FN_TQL_DUCKDB
#include "duckdb.h"
#include <chrono>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
namespace tql_duckdb {
using namespace data_table;
namespace {
// SQL identifier quote (mismo patron que tql_to_sql).
std::string sql_ident(const std::string& name) {
std::string out;
out.reserve(name.size() + 4);
out += '"';
for (char c : name) {
if (c == '"') out += "\"\"";
else out += c;
}
out += '"';
return out;
}
const char* duckdb_type_for(ColumnType t) {
switch (t) {
case ColumnType::Int: return "BIGINT";
case ColumnType::Float: return "DOUBLE";
case ColumnType::Bool: return "BOOLEAN";
case ColumnType::Date: return "VARCHAR"; // se almacena ISO como texto v1
case ColumnType::Json: return "VARCHAR";
default: return "VARCHAR";
}
}
// SQL literal escape para string.
std::string lit_str(const char* s) {
std::string out = "'";
for (const char* p = s ? s : ""; *p; ++p) {
if (*p == '\'') out += "''";
else out += *p;
}
out += "'";
return out;
}
bool create_and_load(duckdb_connection cn, const TableInput& t, std::string& err) {
// CREATE TABLE
std::string ddl = "CREATE TABLE " + sql_ident(t.name) + " (";
for (size_t i = 0; i < t.headers.size(); ++i) {
if (i > 0) ddl += ", ";
ColumnType ct = (i < t.types.size()) ? t.types[i] : ColumnType::String;
ddl += sql_ident(t.headers[i]) + " " + duckdb_type_for(ct);
}
ddl += ");";
duckdb_result rr;
if (duckdb_query(cn, ddl.c_str(), &rr) == DuckDBError) {
err = duckdb_result_error(&rr);
duckdb_destroy_result(&rr);
return false;
}
duckdb_destroy_result(&rr);
// INSERT rows via VALUES batches (1000 rows/insert).
if (t.rows == 0 || t.cols == 0) return true;
const int batch = 1000;
for (int r0 = 0; r0 < t.rows; r0 += batch) {
int r1 = (r0 + batch < t.rows) ? r0 + batch : t.rows;
std::string ins = "INSERT INTO " + sql_ident(t.name) + " VALUES ";
for (int r = r0; r < r1; ++r) {
if (r > r0) ins += ", ";
ins += "(";
for (int c = 0; c < t.cols; ++c) {
if (c > 0) ins += ", ";
const char* v = t.cells[r * t.cols + c];
if (!v || !*v) { ins += "NULL"; continue; }
ColumnType ct = (c < (int)t.types.size()) ? t.types[c] : ColumnType::String;
if (ct == ColumnType::Int || ct == ColumnType::Float) {
// Asumir parseable; sino DuckDB error.
ins += v;
} else if (ct == ColumnType::Bool) {
ins += (std::strcmp(v, "true") == 0) ? "TRUE" : "FALSE";
} else {
ins += lit_str(v);
}
}
ins += ")";
}
ins += ";";
if (duckdb_query(cn, ins.c_str(), &rr) == DuckDBError) {
err = std::string("INSERT into ") + t.name + ": " + duckdb_result_error(&rr);
duckdb_destroy_result(&rr);
return false;
}
duckdb_destroy_result(&rr);
}
return true;
}
ColumnType type_from_duckdb(duckdb_type t) {
switch (t) {
case DUCKDB_TYPE_BOOLEAN: return ColumnType::Bool;
case DUCKDB_TYPE_TINYINT:
case DUCKDB_TYPE_SMALLINT:
case DUCKDB_TYPE_INTEGER:
case DUCKDB_TYPE_BIGINT:
case DUCKDB_TYPE_HUGEINT:
case DUCKDB_TYPE_UTINYINT:
case DUCKDB_TYPE_USMALLINT:
case DUCKDB_TYPE_UINTEGER:
case DUCKDB_TYPE_UBIGINT:
return ColumnType::Int;
case DUCKDB_TYPE_FLOAT:
case DUCKDB_TYPE_DOUBLE:
case DUCKDB_TYPE_DECIMAL:
return ColumnType::Float;
case DUCKDB_TYPE_DATE:
case DUCKDB_TYPE_TIMESTAMP:
return ColumnType::Date;
default:
return ColumnType::String;
}
}
} // anon
Result execute(const std::string& sql,
const std::vector<std::string>& params,
const std::vector<TableInput>& tables) {
Result out;
auto t0 = std::chrono::steady_clock::now();
duckdb_database db = nullptr;
duckdb_connection cn = nullptr;
if (duckdb_open(nullptr, &db) == DuckDBError) {
out.error = "duckdb_open failed";
return out;
}
if (duckdb_connect(db, &cn) == DuckDBError) {
out.error = "duckdb_connect failed";
duckdb_close(&db);
return out;
}
// Crear y poblar tablas.
for (const auto& t : tables) {
std::string e;
if (!create_and_load(cn, t, e)) {
out.error = e;
duckdb_disconnect(&cn);
duckdb_close(&db);
return out;
}
}
// Preparar + bind params.
duckdb_prepared_statement stmt = nullptr;
if (duckdb_prepare(cn, sql.c_str(), &stmt) == DuckDBError) {
out.error = std::string("prepare: ") + duckdb_prepare_error(stmt);
duckdb_destroy_prepare(&stmt);
duckdb_disconnect(&cn);
duckdb_close(&db);
return out;
}
for (size_t i = 0; i < params.size(); ++i) {
// DuckDB params son 1-based.
if (duckdb_bind_varchar(stmt, (idx_t)(i + 1), params[i].c_str()) == DuckDBError) {
out.error = "bind param fail";
duckdb_destroy_prepare(&stmt);
duckdb_disconnect(&cn);
duckdb_close(&db);
return out;
}
}
duckdb_result res;
if (duckdb_execute_prepared(stmt, &res) == DuckDBError) {
out.error = std::string("execute: ") + duckdb_result_error(&res);
duckdb_destroy_result(&res);
duckdb_destroy_prepare(&stmt);
duckdb_disconnect(&cn);
duckdb_close(&db);
return out;
}
// Materializar resultado en StageOutput.
idx_t cols = duckdb_column_count(&res);
idx_t rows = duckdb_row_count(&res);
out.out.cols = (int)cols;
out.out.rows = (int)rows;
out.row_count = (int)rows;
out.out.headers.reserve(cols);
out.out.types.reserve(cols);
for (idx_t c = 0; c < cols; ++c) {
out.out.headers.emplace_back(duckdb_column_name(&res, c));
out.out.types.push_back(type_from_duckdb(duckdb_column_type(&res, c)));
}
out.out.cell_backing.reserve(rows * cols);
out.out.cells.reserve(rows * cols);
for (idx_t r = 0; r < rows; ++r) {
for (idx_t c = 0; c < cols; ++c) {
char* v = duckdb_value_varchar(&res, c, r);
out.out.cell_backing.emplace_back(v ? v : "");
if (v) duckdb_free(v);
}
}
for (auto& s : out.out.cell_backing) out.out.cells.push_back(s.c_str());
duckdb_destroy_result(&res);
duckdb_destroy_prepare(&stmt);
duckdb_disconnect(&cn);
duckdb_close(&db);
auto t1 = std::chrono::steady_clock::now();
out.duration_ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
return out;
}
} // namespace tql_duckdb
#endif // FN_TQL_DUCKDB
@@ -1,29 +0,0 @@
// tql_duckdb: ejecuta SQL DuckDB sobre TableInputs in-memory.
// Solo se compila si FN_TQL_DUCKDB esta definido. Adapter opcional para
// tql_to_sql emit -> execute. Ver issue 0080.
#pragma once
#ifdef FN_TQL_DUCKDB
#include "data_table_logic.h"
#include <string>
#include <vector>
namespace tql_duckdb {
struct Result {
data_table::StageOutput out;
std::string error; // non-empty si fallo
int row_count = 0;
double duration_ms = 0.0;
};
// Impure: abre DuckDB in-memory, registra tablas como CREATE TABLE + INSERT,
// prepara sql con `?` placeholders bound a `params`, materializa resultado.
Result execute(const std::string& sql,
const std::vector<std::string>& params,
const std::vector<data_table::TableInput>& tables);
} // namespace tql_duckdb
#endif // FN_TQL_DUCKDB
Submodule cpp/apps/runtime_test deleted from 49a9f3273d
-5
View File
@@ -1,5 +0,0 @@
shaders_lab
shaders_lab.exe
build/
*.zip
operations.db*
-35
View File
@@ -1,35 +0,0 @@
add_imgui_app(shaders_lab
main.cpp
compiler.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/gl_loader.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/gl_shader.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/gl_framebuffer.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/fullscreen_quad.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/shader_canvas.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/uniform_parser.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/uniform_panel.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_catalog.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_compile.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_uniforms.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_panel.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_node_editor.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_palette.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/dag_node_previews.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/shaderlab_db.cpp
${CMAKE_SOURCE_DIR}/functions/gfx/code_to_generator.cpp
# Primitivos UI usados por el modal Save-as-generator.
${CMAKE_SOURCE_DIR}/functions/core/modal_dialog.cpp
${CMAKE_SOURCE_DIR}/functions/core/text_input.cpp
${CMAKE_SOURCE_DIR}/functions/core/button.cpp
# fps_overlay, panel_menu, layouts_menu, app_menubar, layout_storage ya
# viven en fn_framework.
)
target_include_directories(shaders_lab PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(shaders_lab PRIVATE imgui_node_editor SQLite::SQLite3)
if(WIN32)
# GUI app: sin consola al lanzar (subsystem:windows / -mwindows)
set_target_properties(shaders_lab PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
@@ -1,231 +0,0 @@
# shaders_lab — proximos pasos: ventana sin decoraciones del SO + botones min/max/close en la MainMenuBar
## Motivacion
Hoy la ventana lleva la titlebar nativa de Windows/Linux ademas de la
MainMenuBar de ImGui (View). Son dos barras consumiendo ~60 px verticales.
Queremos:
1. **Recuperar ese espacio** quitando la titlebar del SO.
2. **Integrar los botones min / max / close** en la MainMenuBar de ImGui
(la que renderiza `panel_menu_cpp_core`), alineados a la derecha.
3. Resultado: una sola barra superior compacta con menus + botones de
ventana, igual que VSCode/Spotify/etc.
Aplicable a **todas las apps** del registry, no solo shaders_lab — debe
materializarse como funcion(es) reusables en `cpp/functions/core/`.
---
## Que hace falta
### 1. Crear la ventana sin decoraciones
Una linea en `cpp/framework/app_base.cpp` (donde se crea la GLFW window,
linea ~37):
```cpp
glfwWindowHint(GLFW_DECORATED, GLFW_FALSE);
```
Detras de un nuevo flag `AppConfig::borderless = false` para que las apps
existentes sigan iguales por defecto.
### 2. Dibujar la titlebar custom
Hoy `panel_menu_cpp_core` ya pinta una `BeginMainMenuBar()` con el menu
"View". Ampliamos esa misma barra con:
- Titulo de la app a la izquierda (antes del primer menu) — opcional.
- Espacio para drag en el centro (la propia barra ya es draggeable
porque ImGui detecta clicks fuera de los items).
- Tres botones a la derecha alineados con `SameLine` + offset:
min (—), max (□ / ⧉ segun estado), close (✕).
Glifos: ImGui::ImDrawList con lineas/rect, sin necesidad de fuente
de iconos. O symbols Unicode si la fuente los soporta.
### 3. Lo que el SO te daba gratis y hay que reimplementar
**Drag de la ventana**
```cpp
// Detectar mouse-down en la titlebar (no sobre items):
if (!ImGui::IsAnyItemHovered() && ImGui::IsMouseDragging(0)) {
double cx, cy; glfwGetCursorPos(window, &cx, &cy);
int wx, wy; glfwGetWindowPos(window, &wx, &wy);
// guardar offset al iniciar drag, aplicar glfwSetWindowPos cada frame
}
```
**Doble-click en titlebar → toggle maximize**
```cpp
if (ImGui::IsMouseDoubleClicked(0) && !ImGui::IsAnyItemHovered()) {
if (glfwGetWindowAttrib(window, GLFW_MAXIMIZED))
glfwRestoreWindow(window);
else
glfwMaximizeWindow(window);
}
```
**Botones**
```cpp
glfwIconifyWindow(window); // min
glfwMaximizeWindow(window) / glfwRestoreWindow(window); // max toggle
glfwSetWindowShouldClose(window, true); // close
```
**Resize desde bordes** — la parte fea. Sin decoraciones GLFW no expone
hit-test de bordes. Hay que detectar mouse en franjas de ~6 px en
los 8 lados, cambiar cursor (`glfwSetCursor` con `GLFW_HRESIZE_CURSOR`
etc.), y arrastrar reposicionando+resizing manualmente.
---
## Lo que se pierde
| Comportamiento | Recuperable |
|---------------------------------------------------------|----------------------------------------------------------------------------|
| Snap zones de Windows (Win+flecha, drag al borde) | Solo con codigo nativo Win32 (`WM_NCHITTEST`) — GLFW no lo expone |
| Aero shake, sombras nativas, animacion de minimizar | No facilmente |
| Snap del WM en Linux (i3/sway/KDE) | Igual |
| Bugs Wayland posicionando ventanas borderless | Si Linux es WSLg/X11 sin problema; en Wayland nativo verificar primero |
| Multi-viewport ImGui (`ConfigFlags_ViewportsEnable`) | Cada ventana secundaria tambien sin titlebar → custom en todas. Mas curro |
| Touch / accesibilidad (lectores de pantalla) | Marginal para nuestro caso |
---
## Plan de implementacion
### Fase 1 — funcion reusable + flag en app_base
**Funcion nueva**: `custom_titlebar_cpp_core` (componente, pure desde el
punto de vista del registry — solo dibuja UI; los efectos GLFW se aplican
fuera o se pasan como callbacks). Idealmente fusionada con
`panel_menu_cpp_core` o coordinada con ella para que el menu y los botones
vivan en la **misma** MainMenuBar.
Opcion A (mejor): extender `panel_menu` con parametros opcionales para
los botones del SO:
```cpp
struct WindowControls {
GLFWwindow* window;
bool show_min = true;
bool show_max = true;
bool show_close = true;
};
bool panel_menu(const char* menu_label,
const PanelToggle* items, std::size_t count,
const WindowControls* controls = nullptr); // opcional
```
Pero esto crea acople de `core` con GLFW. Mejor opcion B:
Opcion B (limpia): funcion separada `custom_titlebar_cpp_core` que se
llama **dentro** del menu existente (despues de los menus, antes del
EndMainMenuBar) usando `ImGui::SameLine` con offset al borde derecho. Y
una funcion auxiliar `window_controls_cpp_core` para los tres botones,
que recibe callbacks (`on_min`, `on_max`, `on_close`) sin saber nada de
GLFW. La app las cablea.
```cpp
namespace fn_ui {
struct WindowButtons {
bool min_clicked = false;
bool max_clicked = false;
bool close_clicked = false;
bool is_maximized = false; // input: pinta el icono correcto
};
// Renderiza tres iconos al borde derecho de la barra activa
// (BeginMainMenuBar o cualquier otro contenedor horizontal).
// Devuelve los flags que clico el usuario.
WindowButtons window_controls(bool is_maximized);
// Drag handler: llamar cada frame cuando mouse esta sobre la barra.
// Devuelve delta a aplicar a la posicion de ventana.
// (signature por afinar)
struct WindowDrag { int dx, dy; bool dragging; };
WindowDrag titlebar_drag_handler();
} // namespace fn_ui
```
La app conecta:
```cpp
auto wb = fn_ui::window_controls(glfwGetWindowAttrib(window, GLFW_MAXIMIZED));
if (wb.min_clicked) glfwIconifyWindow(window);
if (wb.max_clicked) { /* toggle */ }
if (wb.close_clicked) glfwSetWindowShouldClose(window, true);
```
Asi `core` no toca GLFW; `framework/app_base` o cada app cablean lo
nativo.
**Cambio en app_base**:
```cpp
struct AppConfig {
// ...existing...
bool borderless = false; // true → GLFW_DECORATED=false
};
// en run_app():
if (config.borderless) glfwWindowHint(GLFW_DECORATED, GLFW_FALSE);
```
### Fase 2 — integracion en shaders_lab
```cpp
fn::AppConfig cfg;
cfg.borderless = true;
// ...
```
En `render()` despues del `panel_menu("View", ...)`, en la misma barra
(reorganizar para que `panel_menu` no cierre `EndMainMenuBar` y deje
hacer SameLine al borde derecho con `window_controls`).
### Fase 3 (opcional) — resize por bordes
Manejador de hit-test en 8 px alrededor del borde de la ventana.
Cambia cursor con `glfwSetCursor`, en mouse-down inicia resize manual
con `glfwSetWindowSize` + `glfwSetWindowPos`.
### Fase 4 (solo si se nota la perdida) — snap de Windows nativo
`WM_NCHITTEST` via HWND. `#ifdef _WIN32`, `glfwGetWin32Window`,
SetWindowLongPtr para subclassear. Trabajo significativo; postponer
hasta haber medido si la falta de snap molesta de verdad.
---
## Decisiones pendientes para el dia que se haga
1. Resize manual por bordes en v1 o solo arrastre + maximizar.
2. Si hacemos `WM_NCHITTEST` o aceptamos sin snap de Windows.
3. Multi-viewport ImGui: queda off mientras la titlebar sea custom, o se
replica el control en cada secondary window.
4. Forma final del API: `panel_menu` extendido vs `window_controls` aparte
(preferencia actual: aparte, mas limpia).
---
## Mi recomendacion practica
Empezar minimal:
- Borderless ON
- Drag arrastrando la MainMenuBar
- Doble-click maximiza/restaura
- Botones min/max/close al borde derecho
- **Sin resize manual** (la ventana es solo maximizable; util para apps tipo lab/dashboard)
- **Sin WM_NCHITTEST** (sin snap de Windows)
- Multi-viewport off
Eso es 80% del valor con 20% del trabajo. Si despues echamos en falta el
resize manual o el snap, se anaden incremental.
@@ -1,151 +0,0 @@
# shaders_lab — proximos pasos: tipos de datos en las aristas del DAG
## Estado actual
Por cada arista del DAG circula **un solo tipo**: `vec4` (RGBA por pixel).
El DAG no es un grafo de pasadas de render — es una plantilla que se compila
a **un unico fragment shader GLSL 330 core** (ver `cpp/functions/gfx/dag_compile.cpp`).
Cada nodo se compila a `vec4 node_<i>(vec4 a, ..., vec2 uv)`. Las conexiones
del editor son llamadas a funcion dentro del mismo `main()`.
Datos accesibles dentro de cualquier nodo, **sin pasar por aristas**:
- `u_time`, `u_resolution`, `u_mouse` (preamble de `gl_shader`)
- `u_params[64]` (vec4 array global con todos los parametros del DAG)
- `u_preview_target` (int, para thumbnails per-nodo)
- `uv` (pasado explicitamente como ultimo argumento)
Tipos de nodo (`DagKind` en `cpp/functions/gfx/dag_types.h`):
| Kind | num_inputs | Recibe | Produce |
|--------|-----------:|------------------------------|------------------------|
| Gen | 0 | `uv`, `u_params` | `vec4` generado |
| Op | 1 | `vec4 a, vec2 uv` | `vec4` transformado |
| Blend | 2 | `vec4 a, vec4 b, vec2 uv` | `vec4` combinado |
| Output | 1 | (sumidero) | `fragColor` directo |
---
## Tier 0 — Pins tipados (otros tipos GLSL escalares/vectoriales)
Mismo modelo single-pass. Solo hay que **declarar tipo por pin** y ajustar
el fallback de input vacio en `dag_compile.cpp:75-89`.
| Tipo | Para que | Coste |
|--------|--------------------------------------------------|----------|
| float | mascaras, alpha, heightmaps, distance fields | trivial |
| vec2 | UVs deformadas, gradientes, flow fields | trivial |
| vec3 | normales, posiciones, color sin alpha | trivial |
| mat2/3 | warps, rotaciones, transforms 2D | trivial |
Desbloquea **Op de displacement**: nodo que toma `vec2` (offset) + `vec4`
(textura) y devuelve `vec4` muestreado con offset.
Cambios necesarios:
- `DagNodeDef` declara tipo por pin de entrada y por salida
- `dag_compile` genera `node_<i>` con esas firmas
- `dag_node_editor` pinta colores de pin distintos y rechaza conexiones incompatibles
---
## Tier 1 — Imagenes rasterizadas (texturas)
Sigue siendo single-pass. Anade un **Gen `image_load`** con `uniform sampler2D`
y emite `texture(u_img_<i>, uv)` como `vec4`. Lo mismo aplica a:
- **Video**`sampler2D` re-subido cada frame
- **Webcam** → idem
- **Audio FFT**`sampler1D` (espectro) o `sampler2D` con historial
Coste: **medio**. Hace falta gestor de texturas (slot binding, hot-reload,
resize). La primitiva `Framebuffer` ya existe (`gl_framebuffer.h`) y
`gl_shader` ya gestiona uniforms — solo falta el camino de carga PNG/JPG → GL.
---
## Tier 2 — Multi-pass (rompe el modelo de un solo shader)
**Salto arquitectonico**. Hay operaciones imposibles en un solo pase porque
cada nodo solo "ve" su propio pixel:
- Blur real con kernel grande (lee vecinos)
- Downsample / mipmap / piramides
- FFT, convoluciones, dilations
- Bloom, glow, SSAO
- Reaction-diffusion, fluidos, feedback (frame anterior)
- Cualquier filtro que requiera la imagen ya rasterizada
Una arista deja de ser `vec4` y pasa a ser **texture handle (FBO completo)**.
Cada nodo se compila a su propio fragment shader, renderiza a su FBO, y el
siguiente lo muestrea como `sampler2D`.
`dag_compile` cambia de naturaleza: pasa de "compilar a un main()" a
**planificar passes** (orden topologico, asignar FBOs con pool reusable,
ejecutar en orden).
**Modos coexisten**: cada nodo declara `mode: inline` (se inlinea como hoy)
o `mode: pass` (FBO propio). En cruces `pass→inline` se muestrea el FBO; en
`inline→inline` sigue siendo llamada a funcion. Mejor de los dos mundos.
Coste: **alto** pero la base ya esta (`Framebuffer`, `fullscreen_quad`).
Es trabajo de arquitectura, no de OpenGL.
---
## Tier 3 — SDF (Signed Distance Fields)
Sub-dominio aparte. Sigue siendo single-pass. **Mucho retorno por poco
codigo** — solo necesita pins de tipo `float` (Tier 0) + nuevos nodos.
- Aristas llevan `float` (distancia con signo)
- Gens: `sdf_sphere`, `sdf_box`, `sdf_torus`, `sdf_plane`
- Ops: `sdf_smooth_union`, `sdf_intersect`, `sdf_subtract`, `sdf_round`, `sdf_displace`
- Terminator: `sdf_raymarch` toma SDF + material → `vec4` (raymarching dentro del fragment)
Te da **3D real (esferas, mezclas organicas, fractales tipo Mandelbulb) sin
mallas, vertices ni camara fuera del shader**. Es el truco de la mitad de Shadertoy.
---
## Tier 4 — Geometria 3D real
Aqui ya **no es solo un fragment shader**. Necesita:
- Vertex buffers (`GL_ARRAY_BUFFER`) con posiciones, normales, UVs
- Vertex shader + fragment shader emparejados
- Depth buffer
- Matrices `model/view/projection`, camara
- Posiblemente indices, instancing, geometry/tess shaders
El DAG cambia de naturaleza: dos sub-grafos distintos.
1. **Grafo de geometria** (mallas, transforms, materiales, luces)
2. **Grafo de imagen** (post-process del render final)
Forma realista de integrarlo: un nodo "Render 3D Scene" (con mini-DAG
interno de mallas/camara/luces) **produce una textura** que entra en el
grafo 2D existente como cualquier otro `vec4`.
Coste: **muy alto**. Practicamente otra app dentro de la app.
---
## Tier 5 — Compute / particulas / simulaciones
Compute shaders (`GL_COMPUTE_SHADER`), SSBOs, transform feedback. Para
sistemas de particulas con miles/millones de elementos, simulaciones
fisicas, reaction-diffusion masivo, etc.
Coste muy alto, valor medio salvo que el objetivo del lab cambie.
---
## Recomendacion de orden
1. **Pins tipados** (Tier 0) — desbloquea displacement, mascaras, base para SDFs.
2. **Texturas** (Tier 1) — `image_load`, `video`, `webcam`, `audio_fft`.
3. **SDF + raymarch** (Tier 3) — maximo retorno por linea de codigo.
4. **Multi-pass** (Tier 2) — el salto arquitectonico. Permite blur real, bloom, feedback.
5. **Geometria 3D** (Tier 4) — solo si el caso de uso lo justifica; Tier 3 ya cubre mucho 3D estetico.
Tier 2 es el cruce de caminos: hasta ahi puedes ir extendiendo el modelo
actual; a partir de ahi toca redisenar `dag_compile` como planificador de passes.
-825
View File
@@ -1,825 +0,0 @@
# Shader Playground — MVP Spec
> Editor web de shaders GLSL con:
> - **Auto-UI** generada a partir de anotaciones en `uniform`s.
> - **Integración con Claude API** para generar y modificar shaders desde chat.
> - **Registro mínimo de funciones** reutilizables que el LLM puede consultar e inyectar.
> - **Sistema de sidebars modulares** estilo apps de VJing: canvas central protagonista, paneles acoplables/ocultables a los lados.
> - **Output fullscreen** para sesiones de VJing.
>
> Pensado para completarse en un finde (fase A sábado + fase B domingo).
---
## 0. Filosofía y no-objetivos
### Objetivos del MVP
- Escribir GLSL en un editor web, ver el resultado en vivo, con el canvas como protagonista visual.
- Declarar `uniform`s anotados → panel de controles se genera solo (sliders, color pickers, xy-pads, knobs).
- Chat lateral con Claude que genera shaders, los modifica, y **usa el fn-registry como herramienta** para reutilizar código existente.
- Biblioteca de funciones GLSL con búsqueda, tal que el usuario pueda "guardar este fbm" o "guardar este efecto de nube" y reutilizarlo en futuros shaders.
- Guardar/cargar shaders y funciones GLSL en `localStorage`.
- Modo fullscreen del canvas para usar en sesiones reales de VJ.
### NO objetivos (explícitamente fuera del MVP)
- ❌ Backend propio / base de datos / multi-usuario.
- ❌ Visualizaciones matemáticas auxiliares (FFT, campos, derivadas).
- ❌ MIDI / OSC / audio FFT / Syphon / Spout / NDI.
- ❌ Multi-pass / buffers encadenados tipo Shadertoy.
- ❌ Vertex shader custom (solo fullscreen quad fijo).
- ❌ Compute shaders.
- ❌ Fine-tuning del LLM, RAG elaborado, embeddings.
- ❌ Categorías/taxonomía compleja del registry (flat namespace con tags es suficiente).
- ❌ Múltiples shaders simultáneos con crossfade / capas estilo Photoshop.
- ❌ Sidebars flotantes arrastrables tipo Ableton/Resolume (siempre acoplados a los bordes).
Si el MVP se usa de verdad durante un mes, las features de arriba entran en futuras iteraciones **una a una**.
---
## 1. Stack técnico
- **Package manager / runtime dev:** Bun.
- **Build:** Vite.
- **UI:** React 18 + TypeScript strict.
- **Estilos:** Tailwind + shadcn/ui.
- **Iconos:** lucide-react.
- **Estado:** Zustand.
- **Editor de código:** CodeMirror 6 (paquetes `@codemirror/state`, `@codemirror/view`, `@codemirror/legacy-modes` para GLSL).
- **Renderer:** WebGL2 directo (sin regl ni Three.js). Wrapper propio minimal en `src/renderer/`.
- **Layout:** CSS Grid + `react-resizable-panels` para los sidebars acoplados.
- **Color picker:** `react-colorful`.
- **LLM:** Claude API (`@anthropic-ai/sdk`) usando `claude-opus-4-7` con streaming.
- **Persistencia:** `localStorage` directo, envuelto en módulo fino.
### Por qué WebGL2 puro y no regl
- Vamos a hacer cosas específicas (hot-swap de programas, introspección de uniforms activos, manejo fino de errores de compilación con números de línea) que regl abstrae de formas que más tarde querríamos revertir.
- Aprender la API te deja preparado para WebGPU/wgpu en v2.
- El wrapper que necesitamos son ~200 líneas de TypeScript. Aceptable.
### Estructura de carpetas
```
src/
editor/ # CodeMirror wrapper y modo GLSL
renderer/ # WebGL2 wrapper, compile pipeline, fullscreen quad
parser/ # Extracción de uniforms desde GLSL source
registry/ # fn-registry: CRUD, búsqueda, inyección
llm/ # Cliente de Claude + tool definitions + prompt templates
ui/
layout/ # Icon rail, sidebar containers, canvas stage
sidebars/ # CodeSidebar, ControlsSidebar, AgentSidebar, RegistrySidebar
controls/ # Slider, ColorPicker, XYPad, Knob, Toggle (widgets individuales)
components/ # shadcn/ui imports
store/ # Zustand stores (uno dedicado a layout)
storage/ # localStorage wrapper + schema
seed/ # Shaders y funciones de ejemplo que se cargan la primera vez
App.tsx
main.tsx
```
---
## 2. Layout de la aplicación (sistema de sidebars)
### Principios
- **El canvas del preview es siempre el protagonista visual.** Ocupa el área central y nunca se reduce a menos de ~60% del viewport salvo en layouts atípicos. Nada de mandarlo a un rincón.
- **Dos sidebars visibles a la vez** como configuración por defecto: uno a la izquierda (típicamente el Code), uno a la derecha (típicamente los Controls). El resto se invocan cuando hacen falta y reemplazan al que esté en ese lado.
- **Sidebars acoplados a los bordes**, no flotantes ni arrastrables. Simplicidad > flexibilidad en MVP.
- **Ancho de sidebar arrastrable** (min 240px, max ~600px), persistido por sidebar.
- **Toggle suave** (show/hide con animación corta, 150ms).
### Zonas y componentes
```
┌──┬────────────────────┬────────────────────────┬────────────────────┐
│ │ │ │ │
│ │ │ │ │
│I │ Left sidebar │ Canvas (preview) │ Right sidebar │
│c │ (CODE o │ WebGL2 fullscreen │ (CONTROLS o │
│o │ REGISTRY) │ quad │ AGENT) │
│n │ │ │ │
│ │ │ │ │
│R │ │ │ │
│a │ │ │ │
│i │ │ │ │
│l │ │ │ │
│ │ │ │ │
└──┴────────────────────┴────────────────────────┴────────────────────┘
```
### Icon rail (columna vertical fija, siempre visible)
Ancho ~48px en el borde izquierdo. Iconos verticales con `lucide-react`:
- 📄 **Code** (ícono `FileCode2`) — toggle del CodeSidebar.
- 🎛️ **Controls** (ícono `Sliders`) — toggle del ControlsSidebar.
- 💬 **Agent** (ícono `Sparkles` o `MessageSquare`) — toggle del AgentSidebar.
- 📚 **Registry** (ícono `Library` o `BookOpen`) — toggle del RegistrySidebar.
- ─── separador ───
- 💾 **Shaders** (ícono `Save`) — abre el panel de shaders guardados (también es un sidebar, en el lado opuesto al que esté libre).
- ⚙️ **Settings** (ícono `Settings`) — abre modal de settings (API key, modelo, tema).
- ⏏️ **Fullscreen** (ícono `Maximize2`) — entra en modo fullscreen VJ.
Cada botón del rail muestra un indicador visual si su sidebar está activo (punto de color al lado del icono, o fondo resaltado).
### Reglas de apertura de sidebars
Cada sidebar tiene un "lado preferido":
- `CODE` → izquierda (preferente).
- `CONTROLS` → derecha (preferente).
- `AGENT` → derecha (preferente).
- `REGISTRY` → izquierda (preferente).
- `SHADERS` → izquierda (preferente).
Al pulsar el icono:
1. Si ese sidebar ya está abierto → cerrarlo.
2. Si no está abierto → abrirlo en su lado preferido, sustituyendo lo que hubiera en ese lado.
3. Modificador `Alt + click` sobre el icono → abrirlo en el lado opuesto (forzar).
Solo puede haber **un sidebar por lado**. No se apilan, no hay tabs superpuestos en el MVP.
### Estado del layout en Zustand
```ts
type SidebarId = 'code' | 'controls' | 'agent' | 'registry' | 'shaders';
type Side = 'left' | 'right';
interface LayoutState {
sidebars: {
left: SidebarId | null;
right: SidebarId | null;
};
widths: Record<Side, number>; // px, persistido
fullscreen: boolean;
toggle: (id: SidebarId, opts?: { forceSide?: Side }) => void;
close: (side: Side) => void;
setWidth: (side: Side, width: number) => void;
enterFullscreen: () => void;
exitFullscreen: () => void;
}
```
### Layout por defecto al primer arranque
```
Left: CODE
Right: CONTROLS
```
Esto es el "layout trabajo": editor + controles en vivo alrededor del canvas. Persiste en localStorage.
### Modo fullscreen (modo VJ)
- Icon rail, sidebars y topbar desaparecen completamente.
- Canvas ocupa el 100% del viewport.
- Atajos siguen funcionando: `F` o `Esc` salen.
- Teclas `1..9` cargan shaders guardados por índice (útil para directo).
- **Overlay botón transparente** en esquina inferior derecha (icono `Maximize2` inverso, solo visible al mover el ratón en los últimos ~2 segundos, fade-out después). Click → salida de fullscreen. Esto es el "como en Resolume": para cuando estás tocando y quieres volver sin buscar tecla.
- Para ajustar uniforms en fullscreen sin salir, el camino es salir con `Esc`, ajustar, y volver a `F`. El MVP no tiene edge panels translúcidos — eso se evaluará en v2 tras uso real.
### Topbar (fuera de fullscreen)
Encima del canvas, arriba del todo, ~40px de alto:
- Izq: nombre del shader actual (editable en línea con doble click).
- Centro: botones Play / Pause / Reset time.
- Der: indicador de compile (verde OK / rojo con `error on line X`), botón nuevo, selector de shader (dropdown).
---
## 3. Contenido de cada sidebar
### CODE sidebar
- Header: título "Code" + indicador de modo actual (sidebar / overlay).
- Body: CodeMirror 6 con GLSL syntax highlight, números de línea, error underlining.
- Footer: info line con bytes, líneas, último compile time, estado (OK / Error línea N).
**Dos modos de visualización, elegibles en Settings:**
1. **Sidebar mode** (default): el editor vive acoplado al borde izquierdo, como cualquier otro sidebar. Coexiste con el preview y los controles. Es el modo "trabajo".
2. **Overlay mode** (estilo apps VJ): al activar CODE, en lugar de abrir un sidebar, aparece un **modal semitransparente** (70% opacidad, fondo oscurecido) flotante sobre el canvas. El canvas sigue renderizando por debajo. `Esc` o click fuera cierra el modal. Es el modo "live coding / VJ" donde el canvas al máximo es lo importante y el código es una ventana que aparece y desaparece.
La elección vive en el modal de Settings (ver §9). El usuario puede cambiar entre modos en cualquier momento. El comportamiento del icono "Code" en el rail se adapta automáticamente: en sidebar mode abre el sidebar, en overlay mode abre el modal.
**Atajo `Cmd/Ctrl + /`** — independientemente del modo configurado, abre el editor en overlay temporalmente. Útil para un vistazo rápido sin cambiar preferencia.
### CONTROLS sidebar
- Header: "Controls" + botón "Reset to defaults" (devuelve todos los uniforms a su `default` del shader actual).
- Body: lista vertical de widgets autogenerados desde los uniforms anotados. Cada uniform es una "card" con:
- Nombre del uniform.
- Widget (Slider / ColorPicker / XYPad / Knob / Toggle / Slider2D).
- Valor actual formateado numéricamente.
- Footer: contador "N uniforms detected".
- Scroll vertical si no caben.
Si el shader no tiene uniforms anotados: mensaje placeholder *"Declare uniforms with `// @slider ...` annotations to see controls here."* con un link "See annotation format" que abre un popover con ejemplos.
### AGENT sidebar
- Header: "Agent" + selector de modelo (dropdown: Opus/Sonnet/Haiku) + botón "Clear conversation".
- Body: lista de mensajes con markdown rendering, bloques de código GLSL con highlight, colapsables para `tool_use` y `tool_result` ("🔧 Searched registry: 3 results").
- Botón "Apply this shader" junto a bloques de código en respuestas del LLM (ver §7).
- Below body: chips con prompts de demostración.
- Footer: textarea de input multilínea, `Cmd/Ctrl + Enter` envía, `Shift + Enter` nueva línea.
### REGISTRY sidebar
- Header: "Functions" + input de búsqueda (filtra en vivo por name/description/tags).
- Body: lista de funciones registradas. Cada item:
- Nombre + signature.
- Tags como pills (color distinto por tag).
- Descripción corta (2 líneas max, truncada).
- Acciones hover: "Insert into current shader" (añade al `@registry_inject_begin/end` markers), "View code" (expande inline), "Edit", "Delete".
- Footer: botón "+ New function" (abre modal de creación), "Import/Export JSON".
### SHADERS sidebar
- Header: "Saved shaders" + botón "+ New".
- Body: lista de shaders guardados. Cada item:
- Thumbnail pequeño (64x36 px) generado rasterizando el shader en un canvas offscreen al guardar.
- Nombre + fecha de última edición.
- Acciones hover: "Load", "Rename", "Duplicate", "Delete", "Export".
- Search por nombre en la cabecera.
Los thumbnails son una mejora visual importante para VJing (reconocimiento instantáneo). Si no da tiempo, fallback a iconos/gradientes generados deterministicamente desde el nombre (tipo GitHub identicons).
---
## 4. Invocación de sidebars
### Canal principal: icon rail
El **icon rail vertical permanente** en el borde izquierdo es el único camino de descubrimiento y uso habitual. Siempre visible (excepto en fullscreen VJ). Click en el icono → toggle del sidebar. Alt+click → abrir en lado opuesto al preferido.
El rail es la fuente de verdad del layout: cualquier usuario, sin leer documentación, sabe qué sidebars existen y puede abrirlos con un click.
### Atajos de teclado (para usuarios avanzados)
Existen pero no son el canal principal — duplican funcionalidad del rail para quien quiera manos en el teclado:
- `F1..F5` — toggle de cada sidebar (ver §11 para la tabla completa).
- `Cmd/Ctrl + B` — colapsar ambos sidebars (canvas máximo sin fullscreen).
- `F` — fullscreen VJ.
- `Esc` — cerrar lo que esté abierto en cascada.
### Modo VJ: botón flotante para salir de fullscreen
En modo fullscreen, al mover el ratón aparece un único botón flotante translúcido en la esquina inferior derecha para salir. Fade-out tras 2s. No hay más invocaciones flotantes — todo lo demás via `Esc` o teclas de atajo.
---
## 5. Renderer (WebGL2 puro)
### Fullscreen quad fijo
Vertex shader no editable:
```glsl
#version 300 es
in vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}
```
Geometría: dos triángulos cubriendo `[-1,1]²`.
### Fragment shader — lo escribe el usuario
El wrapper antepone automáticamente:
```glsl
#version 300 es
precision highp float;
out vec4 fragColor;
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
```
Estos tres uniforms siempre están disponibles, el parser los ignora (no aparecen en controls).
### Render loop
- `requestAnimationFrame`, `performance.now()``u_time` en segundos.
- Play/pause congela/descongela `u_time`.
- Reset pone `u_time = 0`.
- Canvas se redimensiona vía `ResizeObserver` al tamaño del stage central (cambia cuando se abren/cierran sidebars).
### Compile pipeline
- Al cambiar source: debounce 250 ms.
- `gl.createShader``gl.shaderSource``gl.compileShader`.
- Si `COMPILE_STATUS` es false: `gl.getShaderInfoLog()`, parsear línea (formato `ERROR: 0:<line>: <msg>`), propagar al editor.
- Si compila: link program, swap atómico con el anterior, delete del anterior.
- Si el programa nuevo falla: mantener el anterior, canvas NUNCA se queda en negro por un error.
- Introspección post-link: `gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS)` para validar que el parser encontró lo mismo que GLSL realmente expone. Discrepancias → warning en consola (no error).
### Wrapper API
```ts
interface Renderer {
compile(source: string): Promise<CompileResult>;
setUniform(name: string, value: number | number[] | boolean): void;
setPlaying(playing: boolean): void;
resetTime(): void;
resize(w: number, h: number): void;
snapshot(w: number, h: number): Promise<ImageBitmap>; // para thumbnails
dispose(): void;
}
type CompileResult =
| { ok: true; activeUniforms: string[] }
| { ok: false; line: number; message: string };
```
---
## 6. Parser de uniforms
### Formato de anotación
```glsl
uniform float u_speed; // @slider min=0 max=5 default=1
uniform float u_freq; // @slider min=0.1 max=100 default=10 log=true
uniform vec3 u_colorA; // @color default=0.1,0.2,0.5
uniform vec4 u_tint; // @color default=1,0.5,0,1
uniform vec2 u_origin; // @xy min=-1 max=1 default=0,0
uniform vec2 u_offset; // @slider2d min=-10,-10 max=10,10 default=0,0
uniform float u_angle; // @knob min=0 max=6.283 default=0
uniform int u_iter; // @slider min=1 max=50 default=10 step=1
uniform bool u_debug; // @toggle default=false
```
### Algoritmo (regex, suficiente para MVP)
Para cada línea:
1. Match `^\s*uniform\s+(\w+)\s+(\w+)\s*;\s*(?:\/\/\s*@(\w+)(.*))?$`
2. Grupo 1: tipo GLSL. Grupo 2: nombre. Grupo 3: widget kind (opcional). Grupo 4: resto de props.
3. Si no hay widget kind, usar defaults:
- `float``slider(min=0, max=1, default=0)`
- `vec2``xy(min=0,0 max=1,1 default=0.5,0.5)`
- `vec3``color(default=1,1,1)`
- `vec4``color(default=1,1,1,1)`
- `int``slider(step=1, min=0, max=10, default=0)`
- `bool``toggle(default=false)`
4. Parse props `key=value` separados por whitespace. Números: `parseFloat`. Vectores: split por `,`. Bools: `"true"/"false"`.
5. Ignorar uniforms con nombre en `{u_resolution, u_time, u_mouse}` (reservados).
6. Ignorar uniforms cuyo tipo sea `sampler2D` (no soportados, warning en consola).
### Tipos TS
```ts
type GLSLType = 'float' | 'vec2' | 'vec3' | 'vec4' | 'int' | 'bool';
type WidgetKind = 'slider' | 'slider2d' | 'color' | 'xy' | 'knob' | 'toggle';
interface UniformDescriptor {
name: string;
glslType: GLSLType;
widget: WidgetKind;
props: Record<string, number | number[] | boolean>;
defaultValue: number | number[] | boolean;
}
type ParseResult = { uniforms: UniformDescriptor[]; warnings: string[] };
```
### Tests (Vitest, en `src/parser/parser.test.ts`)
1. Uniform sin anotación → defaults por tipo.
2. `@slider min=0 max=10 default=5`.
3. `@color default=1,0,0` sobre `vec3`.
4. `@xy default=0.5,0.5` sobre `vec2`.
5. Uniforms reservados ignorados.
6. Comentario malformado → fallback a defaults, warning.
7. Múltiples uniforms en el mismo source, orden preservado.
8. Comentarios de bloque `/* ... */` no interfieren.
---
## 7. Widgets de control
Todos reciben `{value, onChange, descriptor}`, **controlados**, sin estado interno.
### Slider (float, int)
- shadcn/ui `<Slider>`.
- Label con valor numérico, click para editar.
- Si `log=true`: mapeo logarítmico.
- Si `step` definido: respetarlo.
### ColorPicker (vec3, vec4)
- `react-colorful` `RgbaColorPicker`.
- Almacena internamente como array `[r,g,b]` o `[r,g,b,a]` en `[0,1]`.
### XYPad (vec2 con @xy)
- Cuadrado ~150×150 px.
- Drag con `pointerdown/move/up`.
- Mapea `[0,1]²` del DOM a `[min.x, max.x] × [min.y, max.y]` con Y invertida.
- Valores numéricos debajo.
### Slider2D (vec2 con @slider2d)
- Dos sliders apilados con labels `x` / `y`.
### Knob (float con @knob)
- Círculo SVG con marca.
- Drag vertical u horizontal cambia el valor.
- Visual distinto al slider para que se distinga.
### Toggle (bool)
- shadcn/ui `<Switch>`.
---
## 8. fn-registry (diseñado para ser usado por el LLM)
Biblioteca mínima de funciones GLSL reutilizables, guardadas en `localStorage` y consultables tanto por el usuario (REGISTRY sidebar) como por el LLM (vía tool use).
### Modelo de datos
```ts
interface RegisteredFunction {
id: string; // nanoid
name: string; // nombre GLSL, e.g. "hash12"
signature: string; // "float hash12(vec2 p)"
description: string; // 1-2 frases: qué hace
tags: string[]; // ["noise", "hash"]
body: string; // cuerpo GLSL completo (función entera, firma incluida)
dependencies: string[]; // nombres de otras funciones del registry que usa
createdAt: number;
updatedAt: number;
}
```
### Operaciones
- `list()``RegisteredFunction[]`.
- `search(query: string)``RegisteredFunction[]` (match en name, description, tags).
- `get(name: string)``RegisteredFunction | null`.
- `save(fn: RegisteredFunction)` → upsert.
- `delete(id: string)` → void.
- `resolveDependencies(names: string[])` → devuelve el conjunto cerrado transitivo ordenado topológicamente.
### Inyección en el shader actual
Se usa el patrón de **markers**: el shader tiene un bloque marker, y el renderer — antes de compilar — reemplaza el contenido entre markers con el cuerpo de las funciones declaradas más sus dependencias transitivas.
```glsl
// @registry_inject_begin
// hash12, perlin2d, rotate2d
// @registry_inject_end
void main() { ... }
```
Si el usuario edita dentro del bloque manualmente, se regenera al guardar (con confirmación si hay cambios).
### Semilla inicial (seed)
Cargar ~15 funciones clásicas la primera vez que se abre la app. Mínimo:
- `hash11`, `hash12`, `hash22` (hashes deterministas sin `sin()`).
- `value_noise_2d`, `perlin_noise_2d`, `simplex_noise_2d`.
- `fbm` (fractal brownian motion).
- `rotate2d`.
- `sdf_circle`, `sdf_box`, `sdf_line`.
- `smoothmin`.
- `palette` (Inigo Quilez cosine palette).
- `hsv2rgb`, `rgb2hsv`.
Cada una con `tags` y `description` relevantes para que el LLM pueda buscarlas semánticamente.
### Panel Functions (ya descrito en §3 como REGISTRY sidebar)
---
## 9. Integración con Claude (LLM)
### Configuración
- Usuario pega su API key en un modal de Settings → se guarda en `localStorage` (warning claro: "se guarda en local, no la uses en ordenadores compartidos").
- Selector de modelo: `claude-opus-4-7` (default, mejor calidad), `claude-sonnet-4-6` (más rápido), `claude-haiku-4-5-20251001` (muy rápido, para iteración).
### Cliente
- `@anthropic-ai/sdk` con `dangerouslyAllowBrowser: true`.
- Streaming siempre activo.
- Historial de conversación persistido en `localStorage` (último N mensajes, truncable).
### System prompt (template)
```
You are a creative shader programmer helping the user write WebGL2 fragment shaders for visual art and VJing.
The host environment provides these uniforms automatically — never redeclare them:
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
The target is WebGL2 / GLSL ES 3.00. Use `fragColor` as the output (it's predeclared as `out vec4 fragColor`). The `#version 300 es` directive is prepended automatically — don't include it.
When you declare uniforms the user should be able to tweak, annotate them with a magic comment so the UI generates a control automatically. Supported annotations:
uniform float u_speed; // @slider min=0 max=5 default=1
uniform float u_freq; // @slider min=0.1 max=100 default=10 log=true
uniform vec3 u_color; // @color default=0.1,0.2,0.5
uniform vec4 u_tint; // @color default=1,1,1,1
uniform vec2 u_pos; // @xy min=-1 max=1 default=0,0
uniform float u_angle; // @knob min=0 max=6.283 default=0
uniform bool u_debug; // @toggle default=false
Guidelines:
- Prefer to REUSE functions from the registry when possible. You have tools to search and insert registry functions.
- Keep shaders self-contained and working on first compile.
- Use functional style: pure functions, no side effects inside helpers, compose via explicit parameters.
- When producing a complete shader, annotate uniforms the user is likely to want to tweak live.
- Prefer hash functions that don't rely on `sin()` (use hash12/hash22 from the registry).
- If the user asks for a modification, return the full updated shader via apply_shader, not a diff.
- Keep aspect-ratio correctness in mind: use `(gl_FragCoord.xy - 0.5*u_resolution.xy) / u_resolution.y` for centered, non-stretched coordinates unless a different framing is asked for.
Tools available:
- search_registry(query): find functions by name/description/tags.
- get_function(name): retrieve a function's full body.
- list_registry(): list all available function names and signatures.
- apply_shader(source): replace the user's current shader with this source. Use this when the user explicitly asks you to generate or modify their shader.
- save_function({name, signature, description, tags, body, dependencies}): add a function to the registry.
```
### Tools (Anthropic tool use)
```ts
const tools = [
{
name: 'search_registry',
description: 'Search for reusable GLSL functions in the local registry by name, description, or tags.',
input_schema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
},
{
name: 'get_function',
description: 'Retrieve the full body of a registered function by name.',
input_schema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
},
{
name: 'list_registry',
description: 'List all functions in the registry with their signatures and tags.',
input_schema: { type: 'object', properties: {} },
},
{
name: 'apply_shader',
description: 'Replace the user\'s current fragment shader with new source. Use when the user asks to generate or modify their shader.',
input_schema: { type: 'object', properties: { source: { type: 'string' } }, required: ['source'] },
},
{
name: 'save_function',
description: 'Save a reusable GLSL function to the registry so it can be used in future shaders.',
input_schema: {
type: 'object',
properties: {
name: { type: 'string' },
signature: { type: 'string' },
description: { type: 'string' },
tags: { type: 'array', items: { type: 'string' } },
body: { type: 'string' },
dependencies: { type: 'array', items: { type: 'string' } },
},
required: ['name', 'signature', 'body'],
},
},
];
```
### Loop de tool use
Loop estándar de Anthropic: mandar mensaje con `tools`, si `stop_reason === 'tool_use'` ejecutar las tools, añadir resultados como `tool_result`, volver a llamar al API, repetir hasta `stop_reason === 'end_turn'`.
Las tools son **locales y síncronas**: todas tocan `localStorage` o el store Zustand. No hay red más allá de la llamada al API de Anthropic.
### Confirmación antes de aplicar
- `apply_shader` **no reemplaza silenciosamente** el código actual. Muestra un diff side-by-side y el usuario confirma. Crítico para que el LLM no borre trabajo del usuario.
- `save_function` aplica directamente (es aditivo, no destructivo), pero muestra toast con undo.
### Prompts de demostración (chips en el AgentSidebar)
- "Make a lava lamp shader"
- "Add audio-reactive colors using u_time"
- "Refactor the current shader to use registry functions"
- "Explain what this shader does line by line"
- "Create a kaleidoscope with 8 segments"
---
## 10. Persistencia (localStorage)
### Schema
```ts
interface StorageSchema {
version: 1;
currentShader: string; // id del shader actualmente cargado
shaders: Record<string, {
id: string;
name: string;
source: string;
uniformValues: Record<string, unknown>;
thumbnail?: string; // dataURL pequeño para mostrar en SHADERS sidebar
updatedAt: number;
}>;
functions: Record<string, RegisteredFunction>;
conversations: Array<{
id: string;
messages: Array<{ role: string; content: unknown }>;
updatedAt: number;
}>;
settings: {
apiKey: string | null;
model: string;
theme: 'dark' | 'light';
codeMode: 'sidebar' | 'overlay'; // modo del editor: acoplado o flotante
};
layout: {
sidebars: { left: string | null; right: string | null };
widths: { left: number; right: number };
};
}
```
### Claves
- Una única clave root: `shader-playground:v1`.
- Migraciones: si se encuentra `version < 1` o key vieja, crear backup con sufijo timestamp y regenerar.
- Debounce 500 ms en todas las escrituras.
- API key jamás incluida en exports/imports manuales del usuario.
---
## 11. Atajos de teclado (completos)
### Sidebars
- `F1` — toggle CODE sidebar.
- `F2` — toggle CONTROLS sidebar.
- `F3` — toggle AGENT sidebar.
- `F4` — toggle REGISTRY sidebar.
- `F5` — toggle SHADERS sidebar.
- `Cmd/Ctrl + B` — colapsar ambos sidebars (canvas máximo, no fullscreen).
- `Cmd/Ctrl + /` — abrir CODE como modal overlay sobre canvas.
### Render / modo VJ
- `F` — toggle fullscreen VJ.
- `Esc` — cerrar modal → cerrar sidebars → salir fullscreen (en cascada).
- `Space` (fuera del editor) — play/pause.
- `Cmd/Ctrl + R` — reset time.
- `1..9` (en fullscreen) — cargar shader guardado N.
### Trabajo
- `Cmd/Ctrl + S` — guardar snapshot inmediato.
- `Cmd/Ctrl + Enter` — forzar recompile (desde editor) o enviar mensaje (desde chat).
- `Cmd/Ctrl + K` — focus en chat input (abre AGENT si está cerrado).
---
## 12. Criterios de aceptación
### Fase A — Sábado (core + layout)
- [ ] Editor GLSL funcional con CodeMirror y highlight.
- [ ] WebGL2 renderer con fullscreen quad, hot-recompile con debounce.
- [ ] Error de compilación con línea, canvas mantiene el último válido.
- [ ] Parser de uniforms con todas las anotaciones funcionando.
- [ ] 6 widgets de control conectados.
- [ ] Icon rail visible con 6-7 iconos y toggles funcionales.
- [ ] Los 4 sidebars principales (CODE, CONTROLS, SHADERS, settings modal) funcionan con toggle y ancho redimensionable.
- [ ] Layout default (CODE izq + CONTROLS der) al primer arranque.
- [ ] Setting `codeMode` (sidebar / overlay) funciona: al cambiarlo en Settings, el icono "Code" del rail abre el editor en el modo elegido.
- [ ] Estado del layout persiste en localStorage.
- [ ] Cambiar un slider actualiza el render sin lag.
- [ ] Persistencia: recarga la página y vuelve todo igual, incluidos qué sidebars estaban abiertos.
- [ ] 4 shaders de ejemplo cargan correctamente.
- [ ] Fullscreen funciona con `F` y `Esc`, icon rail y sidebars desaparecen limpiamente.
- [ ] Modo "code overlay" funciona cuando está activado en Settings: el icono "Code" abre un modal flotante sobre el canvas en vez del sidebar.
- [ ] Tests del parser pasan.
**Si la fase A está completa y funciona, ya hay algo usable.** La fase B es aditiva.
### Fase B — Domingo (LLM + registry)
- [ ] REGISTRY sidebar con búsqueda, lista filtrable, acciones "insert / view / edit / delete".
- [ ] Seed inicial de ~15 funciones cargadas al primer arranque.
- [ ] Markers `@registry_inject_begin/end` funcionan, se reemplazan antes de compilar.
- [ ] Modal de Settings para API key de Claude.
- [ ] AGENT sidebar con chat funcional, streaming visible.
- [ ] Tool use funcional (`search_registry`, `get_function`, `list_registry`, `apply_shader`, `save_function`).
- [ ] `apply_shader` muestra diff y pide confirmación.
- [ ] Chips de prompts de demostración.
- [ ] Conversación persiste entre recargas.
### Features opcionales (solo si sobra tiempo)
- [ ] Thumbnails generados en el SHADERS sidebar.
- [ ] Botón overlay flotante en fullscreen para salir (si no, queda `Esc`).
- [ ] Export/import del registry como JSON.
### Criterio global
- [ ] Puedo usar la app para: (1) pedirle a Claude "haz un shader de nubes con double domain warping, guardado como función en el registry", (2) verlo aparecer en el REGISTRY sidebar, (3) abrir un shader nuevo en blanco, (4) pedirle "carga la función de nubes del registry y úsala con una paleta roja-naranja", (5) ajustar los sliders que aparezcan automáticamente, (6) guardarlo, (7) entrar en fullscreen, (8) recargar la página y seguir teniendo todo. **Si este flujo end-to-end no funciona, el MVP no está hecho.**
---
## 13. Orden de implementación
### Sábado (Fase A)
1. Scaffold: Bun init, Vite, React, Tailwind, shadcn CLI, Zustand. Layout con icon rail + stage central + sidebar containers vacíos.
2. WebGL2 wrapper: fullscreen quad, fragment shader hardcoded sólido. Verifica render.
3. CodeMirror con GLSL en CODE sidebar. Source en store. Recompile al cambiar (debounce).
4. Error handling: parse del infoLog, display en editor.
5. Layout store: toggle de sidebars, reglas de lado preferido, persistencia del layout.
6. Parser de uniforms + tests.
7. Store de uniformValues sincronizado con descriptors (diff al cambiar shader).
8. CONTROLS sidebar renderizando widgets autogenerados.
9. Widgets uno a uno: Slider → Toggle → Color → XY → Knob → Slider2D. Cada uno end-to-end.
10. Persistencia localStorage con debounce (shaders + layout + uniformValues).
11. SHADERS sidebar: lista, guardar/cargar/renombrar/duplicar.
12. Shaders de ejemplo en seed.
13. Fullscreen + atajos de teclado + setting `codeMode` con ambas variantes (sidebar y overlay).
### Domingo (Fase B)
14. fn-registry: modelo, CRUD, búsqueda, seed, tests.
15. Markers `@registry_inject_begin/end` y preprocesado antes de compilar.
16. REGISTRY sidebar con búsqueda y acciones.
17. Settings modal con API key.
18. Cliente Claude con streaming (sin tools).
19. AGENT sidebar: chat, markdown rendering, persistencia.
20. Tool definitions y loop de tool use.
21. Diff + confirmación para `apply_shader`.
22. Chips de prompts de demostración.
23. Pulido visual, toasts, mensajes de error amigables.
### Qué sacrificar si algo se alarga (en orden, de menos a más crítico)
1. Thumbnails del SHADERS sidebar (fallback: icon/gradient generado desde el nombre).
2. Botón flotante de salida de fullscreen (queda solo `Esc`).
3. `Slider2D` y `Knob` (los `vec2` usan XY, los `float` normal Slider).
4. Setting `codeMode` — dejar solo modo sidebar; overlay va a v2.
5. Modal de creación de función en registry (solo insert, edit a mano en JSON).
6. Chips de prompts de demostración.
7. Markers `@registry_inject_*` (el LLM pega código directo).
---
## 14. Calidad de código
- TypeScript strict mode, `noImplicitAny`, `strictNullChecks`.
- Módulos `parser/`, `renderer/`, `registry/`, `llm/` son **puros y testables sin DOM ni React**.
- Widgets reciben `value`/`onChange` y son ignorantes del store (el puente se hace en `ControlsSidebar`).
- Render loop NO pasa por React. Subscribe al store de Zustand y lee valores directamente cada frame.
- Cada sidebar es un componente React autocontenido que lee/escribe en su slice del store.
- Vitest para tests del parser y del registry (resolver dependencias transitivas).
- Prettier + ESLint básicos, sin fanatismo.
- Commits en imperativo corto ("Add icon rail", "Wire AGENT sidebar to Claude client").
---
## 15. Lo que NO hay que hacer aunque apetezca
- No añadir audio / MIDI en el MVP.
- No añadir multi-pass "porque es solo un buffer más".
- No refactorizar los widgets a una abstracción genérica antes de tener los 6 implementados.
- No hacer los sidebars flotantes/arrastrables tipo Ableton/Resolume. Siempre acoplados a bordes.
- No permitir más de un sidebar por lado en MVP. Si quieres ver REGISTRY y CONTROLS a la vez, abres uno en cada lado. No tabs apilados.
- No meter un sistema de plugins.
- No añadir LangChain, vector DBs, ni RAG. La tool `search_registry` es un string match simple y es suficiente.
- No hacer backend en Go para persistir "cuando sea". Todo en `localStorage` hasta que duela de verdad.
- No soportar vertex shaders custom.
- No soportar múltiples shaders simultáneos con crossfade. Un shader activo, fullscreen, listo.
Cada una de estas es un día comido y medio MVP menos. Después del MVP y de un mes usándolo de verdad, vuelvo a mirar qué duele y decido.
---
## 16. Referencias útiles
- The Book of Shaders: https://thebookofshaders.com
- Shadertoy: https://www.shadertoy.com
- ISF (precedente de las anotaciones): https://isf.video
- Dave Hoskins "Hash without Sine": https://www.shadertoy.com/view/4djSRW
- Inigo Quilez articles: https://iquilezles.org/articles/
- Resolume / VDMX / KodeLife (referencias de UX de VJing): cómo los sidebars se esconden y la importancia del canvas central.
- Claude API docs: https://docs.claude.com
- `@anthropic-ai/sdk` con navegador: usar `dangerouslyAllowBrowser: true`, warning explícito en la UI sobre la exposición de la API key.
-104
View File
@@ -1,104 +0,0 @@
---
name: shaders_lab
lang: cpp
domain: gfx
description: "Live GLSL shader playground con DAG pipeline. Editor de codigo con compilacion en caliente, panel DAG con paleta de generadores/filtros/output, dos canvas (Code y DAG), parseo de uniforms anotados (// @slider, @color, @xy) que se convierten en controles, persistencia de generators en shaders_lab.db, y guardado/carga de layouts ImGui."
tags: [imgui, opengl, glsl, shaders, dag, live-coding, playground, sqlite]
uses_functions:
# gfx
- gl_loader_cpp_gfx
- gl_shader_cpp_gfx
- gl_framebuffer_cpp_gfx
- fullscreen_quad_cpp_gfx
- shader_canvas_cpp_gfx
- uniform_parser_cpp_gfx
- uniform_panel_cpp_gfx
- dag_catalog_cpp_gfx
- dag_compile_cpp_gfx
- dag_uniforms_cpp_gfx
- dag_panel_cpp_gfx
- dag_node_editor_cpp_gfx
- dag_palette_cpp_gfx
- dag_node_previews_cpp_gfx
- shaderlab_db_cpp_gfx
- code_to_generator_cpp_gfx
# core (modal Save-as-generator)
- modal_dialog_cpp_core
- text_input_cpp_core
- button_cpp_core
uses_types:
- dag_types_cpp_gfx
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/shaders_lab"
repo_url: ""
---
## Arquitectura
App ImGui de live-coding GLSL con dos modos en paralelo:
1. **Code panel** — editor de fragment shader libre. Las anotaciones en
uniforms (`// @slider`, `// @color`, `// @xy`, `// @toggle`) se parsean y
convierten en controles del panel **Controls** que escriben en un
`UniformStore` aplicado al programa cada frame.
2. **DAG panel** — pipeline node-based con catalogo de generadores
(plasma, voronoi, etc.) y filtros (blur, threshold, etc.) que se
compilan a un fragment shader unificado y se renderizan en **Canvas DAG**.
Al guardar un Code shader como "generator" se traduce a un `DagNodeDef` y se
persiste en `shaders_lab.db` (tabla via `shaderlab_db`), apareciendo en la
paleta del DAG junto a los builtins.
## Capas
| Archivo | Responsabilidad |
|---|---|
| `main.cpp` | UI shell, paneles, modal save-as, layouts, AppConfig |
| `compiler.cpp` | `compile_code()`, `compile_dag()`, `mark_code_dirty()` con debounce 250ms |
`main.cpp` mantiene estado global de sesion (g_source, g_pipeline, g_descs,
g_store, g_layouts...) — ImGui retained-mode obliga a que persista entre
frames. Toda la logica pura de compilacion vive en `compiler.cpp` y en las
funciones `dag_compile`, `code_to_generator`, `uniform_parser` del registry.
## Persistencia
- **`shaders_lab.db`** (junto al .exe) — tabla de generators de usuario via
`shaderlab_db_*`, ademas de `imgui_layouts` (creada por `layout_storage`).
- `imgui.ini` y `app_settings.ini` — gestionados por `fn::run_app` en
`<exe_dir>/local_files/`.
## Paneles
| Panel | Atajo | Que muestra |
|---|---|---|
| Code | Ctrl+1 | Editor del fragment shader + boton "Save as generator" |
| DAG Pipeline | Ctrl+2 | Node editor con la pipeline |
| Canvas Code | Ctrl+3 | Render del Code shader |
| Canvas DAG | Ctrl+4 | Render del shader compilado del DAG |
| Controls | Ctrl+5 | Sliders/color pickers de uniforms anotados |
| Functions | Ctrl+6 | Paleta del DAG (generators + filters + output) |
| Generated GLSL | Ctrl+7 | GLSL final del DAG con uniforms baked como const array |
## Build
```bash
# Linux
cd cpp && cmake -B build/linux -S . && cmake --build build/linux --target shaders_lab
# Windows (cross-compile)
cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w64.cmake \
&& cmake --build build/windows --target shaders_lab
```
## Decisiones
- `init_gl_loader = true` (via `fn::run_app` por default cuando se enlaza
con OpenGL) — `shader_canvas`, `gl_shader`, `gl_framebuffer` llaman gl*.
- `viewports = true` — los Canvas se pueden arrastrar fuera del main.
- DAG default: arranca con un nodo "plasma" + "output" si la paleta los
encuentra; persiste el INI con `layout_storage`.
- El boton "Save as generator" valida snake_case, evita colisionar con
builtins, traduce con `code_to_generator`, persiste con `shaderlab_db_save_generator`,
y registra el nodo nuevo en el catalogo en vivo (`dag_register_node`).
-63
View File
@@ -1,63 +0,0 @@
#include "compiler.h"
#include "gfx/shader_canvas.h"
#include "gfx/gl_shader.h"
#include "gfx/uniform_parser.h"
#include "gfx/uniform_panel.h"
#include "gfx/dag_compile.h"
#include "gfx/dag_uniforms.h"
#include <chrono>
#include <string>
#include <vector>
// ── Globals declarados en main.cpp (single source of truth) ─────────────────
extern fn::gfx::ShaderCanvas g_canvas_code;
extern fn::gfx::ShaderCanvas g_canvas_dag;
extern std::string g_source;
extern std::string g_code_err;
extern int g_code_err_line;
extern std::chrono::steady_clock::time_point g_code_last_edit;
extern bool g_code_dirty;
extern std::vector<fn::gfx::UniformDescriptor> g_descs;
extern fn::gfx::UniformStore g_store;
extern std::vector<fn::gfx::DagStep> g_pipeline;
extern std::string g_dag_glsl;
extern std::string g_dag_err;
extern int g_dag_err_line;
namespace shaders_lab {
void compile_code() {
auto r = fn::gfx::compile_fragment(g_source);
if (r.ok) {
g_descs = fn::gfx::parse_uniforms(g_source);
fn::gfx::uniforms_sync(g_store, g_descs);
fn::gfx::canvas_set_program(g_canvas_code, r.program);
g_code_err.clear();
g_code_err_line = -1;
} else {
g_code_err = r.err_msg;
g_code_err_line = r.err_line;
}
}
void compile_dag() {
g_dag_glsl = fn::gfx::compile_dag_to_glsl(g_pipeline);
auto r = fn::gfx::compile_fragment(g_dag_glsl);
if (r.ok) {
fn::gfx::canvas_set_program(g_canvas_dag, r.program);
g_dag_err.clear();
g_dag_err_line = -1;
} else {
g_dag_err = r.err_msg;
g_dag_err_line = r.err_line;
}
}
void mark_code_dirty() {
g_code_last_edit = std::chrono::steady_clock::now();
g_code_dirty = true;
}
} // namespace shaders_lab
-24
View File
@@ -1,24 +0,0 @@
#pragma once
// shaders_lab/compiler — extrae las rutinas impuras de compilacion del shader
// (compile_code, compile_dag, mark_code_dirty) desde main.cpp para que el
// archivo principal quede acotado a la composicion de paneles ImGui.
//
// Las globals (g_source, g_descs, g_store, g_pipeline, etc.) se declaran
// extern y viven en main.cpp; aqui solo orquestamos compilacion.
namespace shaders_lab {
// Compila g_source -> programa OpenGL para g_canvas_code, refresca g_descs
// y sincroniza g_store. Actualiza g_code_err / g_code_err_line.
void compile_code();
// Compila g_pipeline -> g_dag_glsl -> programa OpenGL para g_canvas_dag.
// Actualiza g_dag_err / g_dag_err_line.
void compile_dag();
// Marca el shader Code como dirty y registra el timestamp del ultimo edit
// (para debounce de 250ms en el render loop).
void mark_code_dirty();
} // namespace shaders_lab
@@ -1,254 +0,0 @@
# Shader DAG Lab — Arquitectura y hoja de ruta
Este documento resume el diseño acumulado tras iterar en artifacts desde
"composición funcional sobre listas" hasta "DAG de shaders WebGPU con fan-in".
Sirve como contexto para retomar el proyecto en Claude Code sin perder las
decisiones de diseño.
## El problema que resuelve
Un entorno para componer fragment shaders WGSL visualmente: el usuario arrastra
"nodos" (primitivas shader) desde una paleta a un pipeline, configura
parámetros con sliders / XY pads / color pickers, y el sistema compila el DAG
resultante a un único fragment shader ejecutado en WebGPU.
Las dos vistas — grafo y código — son proyecciones del mismo modelo interno.
El usuario casual arrastra cajas; el usuario avanzado lee el WGSL generado.
## Arquitectura en una frase
```
Pipeline state (árbol JSON)
↓ compileDagToWGSL()
WGSL source
↓ device.createShaderModule()
GPU pipeline
↓ render loop, escribe uniforms cada frame
Canvas
```
Separación crítica: la **topología** del DAG (qué nodos, en qué orden, con qué
aristas) dispara recompilación del shader. Los **valores de parámetros** NO
disparan recompilación — solo se escriben al uniform buffer cada frame. Esto
hace que mover un slider sea instantáneo mientras que añadir/quitar nodos
paga el coste de compilar un nuevo pipeline.
## Modelo de datos
Un `step` del pipeline es:
```ts
type Step = {
id: string; // UUID estable (sobrevive reorders)
name: string; // clave en el catálogo NODES
params: { // valores de parámetros editables
[key: string]: number
};
meta?: { // metadatos que afectan compilación
sourceId?: string; // para blends: id del otro nodo fuente
};
};
```
El `topologyKey` que dispara recompilación se computa como:
```
pipeline.map(s => `${s.name}:${s.meta?.sourceId ?? ''}`).join('|')
```
## Catálogo de nodos
Cada entrada en `NODES` declara:
```ts
{
kind: 'gen' | 'op' | 'blend' | 'warp' | 'sdf' | 'filter' | 'modulator',
label: string,
desc: string,
params: [{ k: string, d: number }], // hasta 4 slots del vec4<f32>
controls: Control[], // descriptores de UI
body: (idx: number) => string, // emite el cuerpo WGSL
}
```
### Tipos de control UI actualmente soportados
- `slider`: rango numérico con thumb
- `xy`: pad 2D que controla dos params contiguos
- `color`: picker RGB que controla tres params contiguos
- `select`: dropdown con opciones discretas (valor = índice)
- `source`: selector del nodo fuente para blends (escribe a `meta.sourceId`)
## Compilación del DAG a WGSL
`compileDagToWGSL(pipeline)` emite un shader con esta estructura:
```wgsl
struct Uniforms {
time: f32,
_pad: f32,
resolution: vec2<f32>,
params: array<vec4<f32>, 16>,
};
@group(0) @binding(0) var<uniform> u: Uniforms;
@vertex fn vs(...) { /* fullscreen triangle */ }
// Una función por nodo, nombrada node_<idx>
fn node_0(c: vec4<f32>, uv: vec2<f32>) -> vec4<f32> { ... }
fn node_1(a: vec4<f32>, b: vec4<f32>, uv: vec2<f32>) -> vec4<f32> { ... } // blend
@fragment fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
let uv = pos.xy / u.resolution;
let out_0 = node_0(vec4<f32>(0.0), uv);
let out_1 = node_1(out_0, out_0, uv); // blend con source=out_0
return out_1;
}
```
Los outputs intermedios `out_<i>` se preservan como variables locales para
que los blends puedan referenciar nodos anteriores arbitrarios. Esto es lo
que hace que el DAG sea más que un pipeline lineal.
## Estado actual de tipos de nodo
**Implementados (lab 004):**
- `gen`: solid, gradient, plasma, checker, circle, stripes, noise (hash)
- `op`: invert, gamma, contrast, saturate, hueShift, tint, posterize, vignette, ripple, pulse
- `blend`: mix, multiply, screen, add, difference, darken, lighten, mask
**Pendientes de implementar (ver hoja de ruta abajo):**
- `warp`: distorsiones de UV (twirl, polar, kaleidoscope, pixelate, chromatic)
- `sdf`: campos de distancia con compositing (smooth_union, subtract, intersect)
- `modulator`: LFOs que producen escalares animados para alimentar parámetros
- Ruidos procedurales reales: perlin, simplex, worley, fbm
- Filtros de luminancia: threshold, levels, duotone, channel_swap
- Inputs externos: mouse como uniform adicional
## Hoja de ruta post-artifact
### Lab 005 — Warps + Ruidos + Filtros de luma + Mouse
Cambios que requiere:
- **Refactor de compilación**: generadores pasan a ser `fn sample_gen_<i>(uv) -> vec4<f32>`
para que los warps puedan modificar uv antes del muestreo.
- La cadena main mantiene dos estados: `uv` (mutable por warps) y `c` (color).
- Nuevo kind `warp` con snippets que transforman `uv`.
- Nuevo uniform `mouse: vec2<f32>` (coords 0..1) actualizado desde pointer events.
- Perlin/FBM como snippets WGSL copiados de implementaciones conocidas
(hash-based gradient noise).
### Lab 006 — Multi-pass (convoluciones + feedback)
Cambio arquitectónico mayor: cada nodo puede escribir a una textura offscreen,
el siguiente samplea esa textura. Requiere:
- Render targets intermedios (pool de texturas)
- Múltiples bind groups
- Double buffer para feedback temporal (frame N+1 lee frame N)
- Detección de qué nodos necesitan aislar su pass y cuáles pueden fusionarse
Desbloquea: blur gaussiano, sobel, edge detection, bloom, reaction-diffusion,
trails, motion blur.
### Lab 007 — SDFs tipados
Introduce heterogeneidad de tipos en las aristas del DAG:
- Aristas de tipo `field` (`f32`) vs `color` (`vec4<f32>`)
- Validación de tipos en compilación: un operador de color no acepta un field
- Nodo terminal `render_sdf` que convierte field → color con shading opciones
(planar, gradient, stroke, inflate/outline)
- Operadores SDF: `smooth_union`, `subtract`, `intersect`, `round`, `onion`
Este es el salto conceptual a "DAG tipado" que formaliza lo que el lab 001
insinuaba (el fold cambiaba de tipo la arista).
### Lab 008 — Bidireccional código ↔ grafo
Hasta ahora solo va grafo → código. El inverso requiere:
- Parser acotado del WGSL que nosotros mismos emitimos (no WGSL general)
- Marcadores en comentarios `// @meta node=<name> id=<id>` para robustez
- Detección de diff estructural para mantener posiciones / parámetros al editar
- Editor de código integrado (CodeMirror) sincronizado con el pipeline
### Lab 009 — Nodos custom definidos por usuario
Modal donde el usuario escribe body WGSL + declara params, y se registra en
la paleta como si viniera del catálogo. Cierra la asimetría código→grafo sin
necesitar parser completo. Persiste en localStorage o export/import JSON.
## Decisiones de diseño que vale la pena recordar
1. **Los IDs de nodo son UUIDs, no índices posicionales**. Las referencias
de `sourceId` sobreviven a reorderings. Los índices se re-derivan en
compilación.
2. **El patrón "armed drag"** para el drag handle: el nodo es `draggable=false`
por defecto y solo se arma a `true` cuando ocurre pointerdown sobre el
header con el handle. Esto evita que los sliders internos activen drag
accidentalmente.
3. **Uniform packing**: todos los parámetros de un nodo van en `u.params[idx]`
(un `vec4<f32>`). Si un nodo necesita más de 4 floats, habría que
reasignar slots o usar dos slots. No hay nodos hasta ahora que lo pidan.
4. **MAX_NODES = 16** es arbitrario, limitado solo por el tamaño del array
de params en el uniform buffer. Subir es trivial: cambia la constante.
5. **Las arbitrary values de Tailwind no funcionan** en algunos entornos
sin JIT. Grid templates, min-h con calc, etc. se escriben con
`style={{...}}` inline. En el proyecto de Claude Code esto no debería
ser problema si usas Tailwind 3+ con su compilador.
## Stack sugerido para Claude Code
- **Vite + React + TypeScript**: setup estándar, HMR inmediato
- **Tailwind 3+** con JIT: los arbitrary values funcionarán esta vez
- **Zustand o Jotai** para el pipeline state (se va a hacer más complejo)
- **Biome o ESLint + Prettier** para formato consistente
- Opcional pero recomendado en cuanto crezca:
- **Vitest** para tests unitarios del compilador (`compileDagToWGSL`)
- **React Testing Library** si tests de UI
- **reactflow** si en lab 008 quieres visualizar el DAG como grafo editable
## Organización de archivos sugerida
```
src/
nodes/
index.ts # export del catálogo completo
generators.ts # gen kind
operators.ts # op kind
blends.ts # blend kind
warps.ts # (lab 005)
sdfs.ts # (lab 007)
types.ts # tipos compartidos NodeDef, Control, etc.
compiler/
compileDagToWGSL.ts # la función principal
uniforms.ts # packing y escritura del buffer
validate.ts # validación de tipos (lab 007+)
webgpu/
useWebGPU.ts # el hook
renderer.ts # setup del device, context, pipeline
ui/
PipelineNode.tsx
controls/
Slider.tsx
XYPad.tsx
ColorPicker.tsx
Select.tsx
SourceSelector.tsx
Palette.tsx
Canvas.tsx
WGSLView.tsx
store/
pipeline.ts # Zustand store
App.tsx
main.tsx
```
## Primeros pasos en Claude Code
1. `npm create vite@latest shader-dag -- --template react-ts`
2. Copiar `shader-dag-blends.jsx` como base monolítica, renombrar a `.tsx`
3. Arreglar los tipos TypeScript (muchas funciones del artifact no están tipadas)
4. Romper el monolito según la estructura de arriba
5. Implementar lab 005: refactor de compilación para habilitar warps
Nota: el artifact de base (`shader-dag-blends.jsx`) funciona pero tiene el
tipado implícito de JSX. Convertir a TS te va a revelar varios tipos que
merece la pena modelar explícitamente (especialmente `Control`, que ahora es
un union discriminado informal).
@@ -1,598 +0,0 @@
import { useState, useMemo, useRef } from 'react';
import { X, Trash2, GripVertical } from 'lucide-react';
//
// Categorías morfológicas cada una con su identidad visual
//
const CATEGORIES = {
gen: {
label: 'Generadores',
subtitle: 'Anamorfismos',
signature: 'seed → [a]',
color: '#7dd3fc',
},
map: {
label: 'Transformaciones',
subtitle: 'Functor · map',
signature: '(a→b) → [a]→[b]',
color: '#c4b5fd',
},
filter: {
label: 'Filtros',
subtitle: 'Refinamientos',
signature: '[a] → [a]',
color: '#fcd34d',
},
scan: {
label: 'Scans',
subtitle: 'Folds acumulativos',
signature: '[a] → [a]',
color: '#86efac',
},
fold: {
label: 'Plegados',
subtitle: 'Catamorfismos',
signature: '[a] → b',
color: '#fda4af',
},
};
// PRNG determinista para que `random` sea reproducible
function mulberry32(seed) {
let a = seed;
return function() {
a = (a + 0x6D2B79F5) | 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
//
// Catálogo de funciones disponibles
//
const FUNCTIONS = {
// GENERADORES (anamorfismos: producen estructura desde un seed)
range: { cat: 'gen', symbol: 'range', sig: 'n → [1..n]', params: [{k:'n',d:20,min:1,max:120}], run: (_,p) => Array.from({length:p.n}, (_,i)=>i+1) },
linspace: { cat: 'gen', symbol: 'linspace', sig: 'n → [0..2π]', params: [{k:'n',d:60,min:2,max:200}], run: (_,p) => Array.from({length:p.n}, (_,i)=>(i/(p.n-1))*2*Math.PI) },
sine: { cat: 'gen', symbol: 'sine', sig: 'n → sin(4π·t)', params: [{k:'n',d:80,min:4,max:200}], run: (_,p) => Array.from({length:p.n}, (_,i)=>Math.sin((i/(p.n-1))*4*Math.PI)) },
noise: { cat: 'gen', symbol: 'noise', sig: 'seed → U(-1,1)', params: [{k:'n',d:50,min:2,max:200},{k:'seed',d:7,min:1,max:9999}], run: (_,p) => { const r=mulberry32(p.seed); return Array.from({length:p.n}, ()=>r()*2-1); } },
fib: { cat: 'gen', symbol: 'fib', sig: 'n → Fibₙ', params: [{k:'n',d:12,min:2,max:25}], run: (_,p) => { const r=[1,1]; while(r.length<p.n) r.push(r[r.length-1]+r[r.length-2]); return r.slice(0,p.n); } },
// MAPS (functor f: ab aplicado punto-a-punto)
double: { cat: 'map', symbol: '·2', sig: 'x ↦ 2x', run: (a) => a.map(x=>x*2) },
square: { cat: 'map', symbol: 'x²', sig: 'x ↦ x²', run: (a) => a.map(x=>x*x) },
negate: { cat: 'map', symbol: '-x', sig: 'x ↦ -x', run: (a) => a.map(x=>-x) },
abs: { cat: 'map', symbol: '|x|', sig: 'x ↦ |x|', run: (a) => a.map(x=>Math.abs(x)) },
sqrt: { cat: 'map', symbol: '√', sig: 'x ↦ √|x|', run: (a) => a.map(x=>Math.sqrt(Math.abs(x))) },
sin: { cat: 'map', symbol: 'sin', sig: 'x ↦ sin x', run: (a) => a.map(x=>Math.sin(x)) },
log: { cat: 'map', symbol: 'ln', sig: 'x ↦ ln(1+|x|)', run: (a) => a.map(x=>Math.log(1+Math.abs(x))) },
// FILTROS (refinamiento)
positive: { cat: 'filter', symbol: '>0', sig: 'x > 0', run: (a) => a.filter(x=>x>0) },
even: { cat: 'filter', symbol: 'par', sig: 'x even', run: (a) => a.filter(x=>Math.round(x)%2===0) },
gt: { cat: 'filter', symbol: '>t', sig: 'x > t', params: [{k:'t',d:0,min:-50,max:50,step:0.5}], run: (a,p) => a.filter(x=>x>p.t) },
// SCANS (folds acumulativos: dejan rastro)
cumsum: { cat: 'scan', symbol: 'Σ*', sig: 'Σ prefix', run: (a) => { let s=0; return a.map(x=>s+=x); } },
cummax: { cat: 'scan', symbol: 'max*', sig: 'max prefix', run: (a) => { let m=-Infinity; return a.map(x=>{m=Math.max(m,x); return m;}); } },
diff: { cat: 'scan', symbol: 'Δ', sig: 'xₙ - xₙ₋₁', run: (a) => a.map((x,i)=>i===0?0:x-a[i-1]) },
mavg: { cat: 'scan', symbol: 'μ_k', sig: 'media móvil k', params: [{k:'k',d:3,min:1,max:15}], run: (a,p) => a.map((_,i)=>{ const s=Math.max(0,i-p.k+1); const sl=a.slice(s,i+1); return sl.reduce((t,v)=>t+v,0)/sl.length; }) },
// FOLDS (catamorfismos: colapsan a escalar · terminales)
sum: { cat: 'fold', symbol: 'Σ', sig: '[a] → Σa', run: (a) => a.reduce((s,x)=>s+x, 0) },
product: { cat: 'fold', symbol: '∏', sig: '[a] → ∏a', run: (a) => a.reduce((s,x)=>s*x, 1) },
max: { cat: 'fold', symbol: 'max', sig: '[a] → max', run: (a) => a.length ? Math.max(...a) : 0 },
min: { cat: 'fold', symbol: 'min', sig: '[a] → min', run: (a) => a.length ? Math.min(...a) : 0 },
mean: { cat: 'fold', symbol: 'μ', sig: '[a] → μ', run: (a) => a.length ? a.reduce((s,x)=>s+x,0)/a.length : 0 },
count: { cat: 'fold', symbol: '#', sig: '[a] → ', run: (a) => a.length },
};
const BY_CATEGORY = Object.entries(CATEGORIES).map(([catKey, catMeta]) => ({
...catMeta,
key: catKey,
items: Object.entries(FUNCTIONS).filter(([, f]) => f.cat === catKey).map(([name, f]) => ({ name, ...f })),
}));
//
// Ejecutor: aplica el pipeline y conserva salidas intermedias
//
function executePipeline(pipeline) {
const steps = [];
let current = Array.from({length: 10}, (_, i) => i + 1); // semilla si no hay generador
let terminated = false;
for (const item of pipeline) {
const def = FUNCTIONS[item.name];
if (!def) continue;
if (terminated) { steps.push({ ...item, def, unreachable: true }); continue; }
try {
const value = def.run(current, item.params || {});
steps.push({ ...item, def, value });
if (def.cat === 'fold') terminated = true;
else current = value;
} catch (e) {
steps.push({ ...item, def, error: e.message });
terminated = true;
}
}
return { steps, terminated };
}
//
// Helpers de id y parámetros
//
let _uid = 0;
const uid = () => `n${++_uid}_${Date.now().toString(36)}`;
function defaultParams(name) {
const def = FUNCTIONS[name];
if (!def.params) return {};
return Object.fromEntries(def.params.map(p => [p.k, p.d]));
}
//
// Sparkline compacto (SVG manual)
//
function Sparkline({ data, color = '#888', w = 180, h = 34 }) {
if (!Array.isArray(data) || data.length === 0) {
return <div style={{width:w, height:h}} className="flex items-center justify-center text-[10px] text-neutral-600"></div>;
}
const min = Math.min(...data);
const max = Math.max(...data);
const span = max - min || 1;
const n = data.length;
const xStep = n > 1 ? (w - 6) / (n - 1) : 0;
const pts = data.map((v, i) => [3 + i * xStep, h - 3 - ((v - min) / span) * (h - 6)]);
const path = pts.map((p, i) => (i === 0 ? `M${p[0].toFixed(1)},${p[1].toFixed(1)}` : `L${p[0].toFixed(1)},${p[1].toFixed(1)}`)).join(' ');
const zeroY = h - 3 - ((0 - min) / span) * (h - 6);
const showZero = min < 0 && max > 0;
return (
<svg width={w} height={h} className="block">
{showZero && <line x1={0} x2={w} y1={zeroY} y2={zeroY} stroke={color} strokeOpacity={0.15} strokeDasharray="2 3" />}
<path d={path} fill="none" stroke={color} strokeWidth={1.3} strokeLinecap="round" strokeLinejoin="round" />
{n <= 40 && pts.map((p, i) => (
<circle key={i} cx={p[0]} cy={p[1]} r={1.4} fill={color} fillOpacity={0.85} />
))}
</svg>
);
}
//
// Ficha en la paleta (draggable)
//
function PaletteItem({ fnName, fn, color, onDragStart, onClick }) {
return (
<div
draggable
onDragStart={(e) => { e.dataTransfer.setData('text/fn-name', fnName); e.dataTransfer.effectAllowed = 'copy'; onDragStart?.(); }}
onClick={onClick}
className="group flex items-center gap-2 px-2.5 py-1.5 rounded-md cursor-grab active:cursor-grabbing select-none transition-colors hover:bg-neutral-800/60"
style={{ borderLeft: `2px solid ${color}` }}
title={`${fnName} :: ${fn.sig}`}
>
<span className="font-mono text-[11px] font-semibold tracking-tight" style={{ color }}>{fn.symbol}</span>
<span className="font-mono text-[11px] text-neutral-400 flex-1 truncate">{fnName}</span>
<span className="font-mono text-[9px] text-neutral-600 opacity-0 group-hover:opacity-100 transition-opacity">drag</span>
</div>
);
}
//
// Nodo del pipeline
//
function PipelineNode({ step, index, isLast, onRemove, onParamChange, onDragStart, onDragOver, onDrop, isDragging }) {
const { def, value, unreachable, error } = step;
const cat = CATEGORIES[def.cat];
const color = cat.color;
const isScalar = def.cat === 'fold';
return (
<div
draggable
onDragStart={(e) => { e.dataTransfer.setData('text/reorder-index', String(index)); e.dataTransfer.effectAllowed = 'move'; onDragStart?.(index); }}
onDragOver={(e) => { e.preventDefault(); onDragOver?.(index); }}
onDrop={(e) => onDrop?.(e, index)}
className="relative group"
style={{ opacity: isDragging ? 0.4 : 1 }}
>
<div
className="flex flex-col rounded-lg backdrop-blur-sm transition-all"
style={{
background: `linear-gradient(180deg, ${color}14 0%, ${color}06 100%)`,
border: `1px solid ${color}40`,
boxShadow: `0 0 0 1px ${color}10, 0 8px 24px -12px ${color}30`,
minWidth: 200,
}}
>
{/* cabecera */}
<div className="flex items-center gap-1.5 px-2.5 py-1.5 border-b" style={{ borderColor: `${color}25` }}>
<GripVertical size={12} className="text-neutral-600 cursor-grab" />
<span className="font-mono text-[11px] font-bold" style={{ color }}>{def.symbol}</span>
<span className="font-mono text-[10px] text-neutral-500 flex-1">{step.name}</span>
<button
onClick={() => onRemove(index)}
className="opacity-0 group-hover:opacity-100 transition-opacity text-neutral-500 hover:text-neutral-200 p-0.5 rounded"
aria-label="Eliminar"
>
<X size={11} />
</button>
</div>
{/* params */}
{def.params && (
<div className="flex items-center gap-1.5 px-2.5 py-1.5 border-b flex-wrap" style={{ borderColor: `${color}18` }}>
{def.params.map(p => (
<label key={p.k} className="flex items-center gap-1 font-mono text-[10px] text-neutral-500">
<span>{p.k}=</span>
<input
type="number"
value={step.params?.[p.k] ?? p.d}
min={p.min} max={p.max} step={p.step ?? 1}
onChange={(e) => onParamChange(index, p.k, Number(e.target.value))}
className="w-12 bg-neutral-900/60 border border-neutral-700/50 rounded px-1 py-0.5 text-neutral-200 font-mono text-[10px] focus:outline-none focus:border-neutral-500"
/>
</label>
))}
</div>
)}
{/* preview */}
<div className="px-2.5 py-1.5">
{unreachable ? (
<div className="font-mono text-[10px] text-neutral-600 italic">inalcanzable · fold anterior terminó el pipeline</div>
) : error ? (
<div className="font-mono text-[10px] text-rose-400">error: {error}</div>
) : isScalar ? (
<div className="flex items-baseline gap-2">
<span className="font-mono text-[10px] text-neutral-500">resultado</span>
<span className="font-display text-2xl font-light" style={{ color }}>{formatScalar(value)}</span>
</div>
) : (
<div>
<Sparkline data={value} color={color} w={180} h={32} />
<div className="font-mono text-[9px] text-neutral-600 mt-1">
n={value?.length ?? 0} · range [{formatScalar(Math.min(...(value?.length?value:[0])))}, {formatScalar(Math.max(...(value?.length?value:[0])))}]
</div>
</div>
)}
</div>
</div>
{/* flecha de composición */}
{!isLast && (
<div className="absolute top-1/2 -right-5 -translate-y-1/2 font-mono text-neutral-600 text-sm pointer-events-none"></div>
)}
</div>
);
}
function formatScalar(v) {
if (v === undefined || v === null || !isFinite(v)) return '—';
if (Number.isInteger(v)) return String(v);
const abs = Math.abs(v);
if (abs >= 1000 || (abs > 0 && abs < 0.01)) return v.toExponential(2);
return v.toFixed(3);
}
//
// Visualización grande del output final
//
function FinalView({ lastStep }) {
if (!lastStep) {
return (
<div className="flex-1 flex items-center justify-center text-neutral-600 font-mono text-xs">
arrastra una función para empezar
</div>
);
}
const { def, value, error, unreachable } = lastStep;
if (error || unreachable) return null;
const color = CATEGORIES[def.cat].color;
if (def.cat === 'fold') {
return (
<div className="flex flex-col items-center justify-center py-6 w-full">
<span className="font-mono text-[10px] uppercase tracking-[0.2em] text-neutral-500 mb-2">resultado · escalar</span>
<span className="font-display text-5xl font-light break-all text-center" style={{ color }}>{formatScalar(value)}</span>
</div>
);
}
// plot grande
if (!Array.isArray(value) || value.length === 0) {
return <div className="font-mono text-xs text-neutral-600 p-4"> (lista vacía)</div>;
}
const W = 720, H = 180, pad = 20;
const min = Math.min(...value), max = Math.max(...value);
const span = max - min || 1;
const n = value.length;
const xStep = n > 1 ? (W - 2 * pad) / (n - 1) : 0;
const pts = value.map((v, i) => [pad + i * xStep, H - pad - ((v - min) / span) * (H - 2 * pad)]);
const path = pts.map((p, i) => (i === 0 ? `M${p[0].toFixed(1)},${p[1].toFixed(1)}` : `L${p[0].toFixed(1)},${p[1].toFixed(1)}`)).join(' ');
const areaPath = `${path} L${pts[pts.length-1][0]},${H-pad} L${pts[0][0]},${H-pad} Z`;
const zeroY = H - pad - ((0 - min) / span) * (H - 2 * pad);
const showZero = min < 0 && max > 0;
return (
<div className="flex flex-col">
<div className="flex items-baseline justify-between mb-2">
<span className="font-mono text-[10px] uppercase tracking-[0.2em] text-neutral-500">resultado · señal</span>
<span className="font-mono text-[10px] text-neutral-500">n={n} · [{formatScalar(min)}, {formatScalar(max)}]</span>
</div>
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-auto" preserveAspectRatio="none">
<defs>
<linearGradient id="areaGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity="0.3" />
<stop offset="100%" stopColor={color} stopOpacity="0" />
</linearGradient>
</defs>
{/* grid */}
{[0.25, 0.5, 0.75].map(f => (
<line key={f} x1={pad} x2={W-pad} y1={pad + f*(H-2*pad)} y2={pad + f*(H-2*pad)} stroke="#fff" strokeOpacity={0.04} />
))}
{showZero && <line x1={pad} x2={W-pad} y1={zeroY} y2={zeroY} stroke={color} strokeOpacity={0.25} strokeDasharray="3 4" />}
<path d={areaPath} fill="url(#areaGrad)" />
<path d={path} fill="none" stroke={color} strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" />
{n <= 80 && pts.map((p, i) => (
<circle key={i} cx={p[0]} cy={p[1]} r={2} fill={color} />
))}
{/* eje */}
<line x1={pad} x2={W-pad} y1={H-pad} y2={H-pad} stroke="#fff" strokeOpacity={0.1} />
<line x1={pad} x2={pad} y1={pad} y2={H-pad} stroke="#fff" strokeOpacity={0.1} />
</svg>
</div>
);
}
//
// APP principal
//
export default function App() {
const [pipeline, setPipeline] = useState([
{ id: uid(), name: 'range', params: defaultParams('range') },
{ id: uid(), name: 'square', params: defaultParams('square') },
{ id: uid(), name: 'cumsum', params: defaultParams('cumsum') },
]);
const [dragIndex, setDragIndex] = useState(null);
const [hoverIndex, setHoverIndex] = useState(null);
const dropZoneRef = useRef(null);
const { steps } = useMemo(() => executePipeline(pipeline), [pipeline]);
const lastStep = steps[steps.length - 1];
// expresión simbólica: fold_sum map_square gen_range(20)
const expression = useMemo(() => {
if (pipeline.length === 0) return '∅';
const parts = pipeline.map(s => {
const def = FUNCTIONS[s.name];
const paramStr = def.params ? '(' + def.params.map(p => `${p.k}=${s.params?.[p.k] ?? p.d}`).join(',') + ')' : '';
return `${s.name}${paramStr}`;
}).reverse();
return parts.join(' ∘ ');
}, [pipeline]);
const addFunction = (name) => {
setPipeline(p => [...p, { id: uid(), name, params: defaultParams(name) }]);
};
const removeFunction = (index) => {
setPipeline(p => p.filter((_, i) => i !== index));
};
const changeParam = (index, key, value) => {
setPipeline(p => p.map((s, i) => i === index ? { ...s, params: { ...s.params, [key]: value } } : s));
};
const handleDropOnZone = (e) => {
e.preventDefault();
const fnName = e.dataTransfer.getData('text/fn-name');
const reorderIdx = e.dataTransfer.getData('text/reorder-index');
if (fnName && FUNCTIONS[fnName]) {
addFunction(fnName);
} else if (reorderIdx !== '') {
// soltar al final si vino de un nodo
const from = Number(reorderIdx);
setPipeline(p => {
const copy = [...p];
const [moved] = copy.splice(from, 1);
copy.push(moved);
return copy;
});
}
setDragIndex(null);
setHoverIndex(null);
};
const handleDropOnNode = (e, targetIdx) => {
e.preventDefault();
e.stopPropagation();
const fnName = e.dataTransfer.getData('text/fn-name');
const reorderIdx = e.dataTransfer.getData('text/reorder-index');
if (fnName && FUNCTIONS[fnName]) {
setPipeline(p => {
const copy = [...p];
copy.splice(targetIdx, 0, { id: uid(), name: fnName, params: defaultParams(fnName) });
return copy;
});
} else if (reorderIdx !== '') {
const from = Number(reorderIdx);
if (from === targetIdx) return;
setPipeline(p => {
const copy = [...p];
const [moved] = copy.splice(from, 1);
const adjusted = from < targetIdx ? targetIdx - 1 : targetIdx;
copy.splice(adjusted, 0, moved);
return copy;
});
}
setDragIndex(null);
setHoverIndex(null);
};
return (
<div className="w-full min-h-screen text-neutral-200" style={{
background: 'radial-gradient(ellipse at top, #0f1419 0%, #06080a 60%, #030506 100%)',
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
}}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
.font-display { font-family: 'Fraunces', serif; font-optical-sizing: auto; }
.font-mono { font-family: 'JetBrains Mono', ui-monospace, monospace; }
.grid-bg {
background-image:
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
background-size: 24px 24px;
}
/* scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.16); }
`}</style>
<div className="grid-bg min-h-screen">
{/* HEADER */}
<header className="px-6 pt-6 pb-5 border-b border-white/5">
<div className="flex items-end justify-between flex-wrap gap-4">
<div>
<div className="flex items-baseline gap-3">
<span className="font-mono text-[10px] uppercase tracking-[0.3em] text-neutral-500">lab · 001</span>
<span className="font-mono text-[10px] text-neutral-700">/</span>
<span className="font-mono text-[10px] text-neutral-500">composición funcional</span>
</div>
<h1 className="font-display text-3xl font-light tracking-tight mt-1.5">
laboratorio de <em className="italic" style={{color:'#c4b5fd'}}>morfismos</em>
</h1>
<p className="font-mono text-[11px] text-neutral-500 mt-1.5 max-w-2xl">
arrastra funciones al pipeline y componlas de izquierda a derecha. cada tipo corresponde a un esquema de recursión distinto.
</p>
</div>
<button
onClick={() => setPipeline([])}
className="flex items-center gap-2 font-mono text-[10px] uppercase tracking-wider text-neutral-500 hover:text-neutral-200 border border-white/10 hover:border-white/30 px-3 py-1.5 rounded transition-colors"
>
<Trash2 size={11} /> vaciar
</button>
</div>
</header>
{/* MAIN GRID · 2 columnas: paleta | (expresión + pipeline + visualizador apilados) */}
<main
style={{
display: 'grid',
gridTemplateColumns: '200px minmax(280px, 1fr)',
minHeight: 'calc(100vh - 140px)',
overflowX: 'auto',
}}
>
{/* ─── IZQUIERDA · PALETA ─── */}
<aside className="border-r border-white/5 p-5 overflow-y-auto" style={{maxHeight: 'calc(100vh - 140px)'}}>
<div className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500 mb-4">primitivas</div>
<div className="flex flex-col gap-5">
{BY_CATEGORY.map(cat => (
<section key={cat.key}>
<div className="flex items-center gap-2 mb-2 px-1">
<span className="w-1.5 h-1.5 rounded-full" style={{background: cat.color, boxShadow: `0 0 8px ${cat.color}`}} />
<span className="font-display text-sm italic" style={{color: cat.color}}>{cat.label}</span>
</div>
<div className="font-mono text-[9px] text-neutral-600 mb-2 px-1 leading-snug">
{cat.subtitle} · <span className="text-neutral-500">{cat.signature}</span>
</div>
<div className="flex flex-col gap-0.5">
{cat.items.map(item => (
<PaletteItem
key={item.name}
fnName={item.name}
fn={item}
color={cat.color}
onClick={() => addFunction(item.name)}
/>
))}
</div>
</section>
))}
</div>
<div className="mt-6 pt-4 border-t border-white/5 font-mono text-[9px] text-neutral-600 leading-relaxed">
tip · click o drag para añadir al pipeline. reordena arrastrando nodos existentes.
</div>
</aside>
{/* ─── DERECHA · EXPRESIÓN + PIPELINE + VISUALIZADOR ─── */}
<section className="p-4 md:p-6 overflow-y-auto flex flex-col gap-5" style={{maxHeight: 'calc(100vh - 140px)'}}>
{/* Expresión simbólica */}
<div>
<div className="flex items-baseline gap-3 mb-2">
<span className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500">expresión</span>
<span className="font-mono text-[9px] text-neutral-600">se aplica de derecha a izquierda</span>
</div>
<div className="rounded-lg bg-white/[0.03] border border-white/10 p-3">
<code className="font-mono text-xs text-neutral-200 break-all leading-relaxed">
{expression}
</code>
</div>
</div>
{/* Pipeline */}
<div>
<div className="flex items-baseline gap-3 mb-2">
<span className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500">pipeline</span>
<span className="font-mono text-[10px] text-neutral-600">{pipeline.length} {pipeline.length === 1 ? 'nodo' : 'nodos'}</span>
</div>
<div
ref={dropZoneRef}
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; }}
onDrop={handleDropOnZone}
className="rounded-xl border border-dashed border-white/10 p-5 transition-colors hover:border-white/20"
style={{background: 'rgba(255,255,255,0.015)', minHeight: '180px'}}
>
{pipeline.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center py-12 text-center">
<div className="font-display text-xl italic text-neutral-500 mb-2">zona de composición</div>
<div className="font-mono text-[10px] text-neutral-600 max-w-xs">arrastra una primitiva desde la izquierda. los nodos se encadenan de izquierda a derecha.</div>
</div>
) : (
<div className="flex flex-wrap gap-x-8 gap-y-4 items-start">
{steps.map((step, i) => (
<PipelineNode
key={step.id}
step={step}
index={i}
isLast={i === steps.length - 1}
onRemove={removeFunction}
onParamChange={changeParam}
onDragStart={setDragIndex}
onDragOver={setHoverIndex}
onDrop={handleDropOnNode}
isDragging={dragIndex === i}
/>
))}
</div>
)}
</div>
</div>
{/* Visualizador */}
<div>
<div className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500 mb-2">visualizador</div>
<div className="rounded-lg border border-white/5 bg-white/[0.015] p-4" style={{minHeight: '220px'}}>
<div className="h-full flex items-center justify-center">
<FinalView lastStep={lastStep} />
</div>
</div>
</div>
{/* Nota didáctica */}
<div className="font-mono text-[10px] text-neutral-600 leading-relaxed border-l-2 border-neutral-800 pl-3">
<span style={{color:'#7dd3fc'}}>anamorfismo</span> genera estructura · <span style={{color:'#c4b5fd'}}>functor map</span> transforma punto a punto ·{' '}
<span style={{color:'#fcd34d'}}>filtro</span> refina · <span style={{color:'#86efac'}}>scan</span> acumula dejando rastro ·{' '}
<span style={{color:'#fda4af'}}>catamorfismo</span> colapsa (es terminal)
</div>
</section>
</main>
</div>
</div>
);
}
File diff suppressed because it is too large Load Diff
@@ -1,878 +0,0 @@
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { X, AlertCircle, RotateCcw, ChevronDown, ChevronRight, Trash2, GripVertical } from 'lucide-react';
//
// CATÁLOGO DE NODOS cada uno emite un snippet WGSL
// Convención: cada nodo compila a `fn node_<idx>(c, uv) -> vec4<f32>`
// Parámetros: hasta 4 floats empaquetados en u.params[idx] (vec4<f32>)
//
const MAX_NODES = 16;
const ACCENT = '#5eead4';
const GEN_COLOR = '#5eead4';
const OP_COLOR = '#c4b5fd';
const NODES = {
// GENERADORES (ignoran c, producen nuevo color)
solid: {
kind: 'gen', label: 'solid', desc: 'color constante',
params: [
{ k: 'r', label: 'r', min: 0, max: 1, d: 0.35, step: 0.01 },
{ k: 'g', label: 'g', min: 0, max: 1, d: 0.25, step: 0.01 },
{ k: 'b', label: 'b', min: 0, max: 1, d: 0.55, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
return vec4<f32>(p.x, p.y, p.z, 1.0);`,
},
gradient: {
kind: 'gen', label: 'gradient', desc: 'gradiente en ángulo',
params: [
{ k: 'angle', label: 'ángulo', min: 0, max: 6.2832, d: 0.8, step: 0.01 },
{ k: 'hue', label: 'tono', min: 0, max: 1, d: 0.5, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
let dir = vec2<f32>(cos(p.x), sin(p.x));
let t = dot(uv - 0.5, dir) + 0.5;
let col = 0.5 + 0.5 * cos(6.28318 * (p.y + vec3<f32>(0.0, 0.33, 0.67) + t));
return vec4<f32>(col, 1.0);`,
},
plasma: {
kind: 'gen', label: 'plasma', desc: 'onda trigonométrica',
params: [
{ k: 'speed', label: 'velocidad', min: 0, max: 3, d: 1, step: 0.01 },
{ k: 'scale', label: 'escala', min: 0.5, max: 10, d: 2, step: 0.1 },
],
body: (i) => `
let p = u.params[${i}];
let t = u.time * p.x;
let col = 0.5 + 0.5 * cos(t + uv.xyx * p.y + vec3<f32>(0.0, 2.0, 4.0));
return vec4<f32>(col, 1.0);`,
},
checker: {
kind: 'gen', label: 'checker', desc: 'tablero rotando',
params: [
{ k: 'scale', label: 'escala', min: 1, max: 30, d: 8, step: 0.5 },
{ k: 'rot', label: 'rotación', min: -2, max: 2, d: 0.25, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
let aspect = u.resolution.x / u.resolution.y;
let q0 = vec2<f32>((uv.x - 0.5) * aspect, uv.y - 0.5);
let a = u.time * p.y;
let rm = mat2x2<f32>(cos(a), -sin(a), sin(a), cos(a));
let q = rm * q0 * p.x;
let chk = (floor(q.x) + floor(q.y)) - 2.0 * floor((floor(q.x) + floor(q.y)) * 0.5);
return vec4<f32>(vec3<f32>(chk), 1.0);`,
},
circle: {
kind: 'gen', label: 'circle', desc: 'sdf de círculo',
params: [
{ k: 'radius', label: 'radio', min: 0, max: 1, d: 0.4, step: 0.01 },
{ k: 'soft', label: 'suavidad', min: 0.001, max: 0.1, d: 0.008, step: 0.001 },
{ k: 'pulse', label: 'pulso', min: 0, max: 1, d: 0.1, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
let aspect = u.resolution.x / u.resolution.y;
let pos = vec2<f32>((uv.x - 0.5) * aspect, uv.y - 0.5);
let r = p.x + p.z * 0.15 * sin(u.time * 2.0);
let d = length(pos) - r;
let fill = smoothstep(p.y, -p.y, d);
return mix(c, vec4<f32>(1.0), fill);`,
},
stripes: {
kind: 'gen', label: 'stripes', desc: 'rayas animadas',
params: [
{ k: 'freq', label: 'frecuencia', min: 1, max: 80, d: 20, step: 0.5 },
{ k: 'speed', label: 'velocidad', min: -5, max: 5, d: 1, step: 0.05 },
{ k: 'angle', label: 'ángulo', min: 0, max: 3.1416, d: 0.5, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
let dir = vec2<f32>(cos(p.z), sin(p.z));
let x = dot(uv, dir);
let v = 0.5 + 0.5 * sin(x * p.x + u.time * p.y);
return vec4<f32>(vec3<f32>(v), 1.0);`,
},
noise: {
kind: 'gen', label: 'noise', desc: 'hash pseudo-aleatorio',
params: [
{ k: 'scale', label: 'escala', min: 1, max: 200, d: 80, step: 1 },
{ k: 'seed', label: 'seed', min: 0, max: 100, d: 7, step: 1 },
],
body: (i) => `
let p = u.params[${i}];
let q = floor(uv * p.x + p.y);
let h = fract(sin(dot(q, vec2<f32>(12.9898, 78.233))) * 43758.5453);
return vec4<f32>(vec3<f32>(h), 1.0);`,
},
// OPERADORES (transforman c)
invert: {
kind: 'op', label: 'invert', desc: '1 rgb',
params: [],
body: () => `
return vec4<f32>(1.0 - c.rgb, c.a);`,
},
gamma: {
kind: 'op', label: 'gamma', desc: 'pow(rgb, γ)',
params: [
{ k: 'g', label: 'γ', min: 0.1, max: 5, d: 1, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
let g = max(0.01, p.x);
return vec4<f32>(pow(max(c.rgb, vec3<f32>(0.0)), vec3<f32>(g)), c.a);`,
},
brightness: {
kind: 'op', label: 'brightness', desc: 'rgb + v',
params: [
{ k: 'v', label: 'valor', min: -1, max: 1, d: 0, step: 0.01 },
],
body: (i) => `
return vec4<f32>(c.rgb + vec3<f32>(u.params[${i}].x), c.a);`,
},
contrast: {
kind: 'op', label: 'contrast', desc: '(rgb 0.5)·k + 0.5',
params: [
{ k: 'k', label: 'k', min: 0, max: 3, d: 1, step: 0.01 },
],
body: (i) => `
return vec4<f32>((c.rgb - vec3<f32>(0.5)) * u.params[${i}].x + vec3<f32>(0.5), c.a);`,
},
saturate: {
kind: 'op', label: 'saturate', desc: 'lerp(luma, rgb, s)',
params: [
{ k: 's', label: 's', min: 0, max: 2, d: 1, step: 0.01 },
],
body: (i) => `
let luma = dot(c.rgb, vec3<f32>(0.2126, 0.7152, 0.0722));
return vec4<f32>(mix(vec3<f32>(luma), c.rgb, u.params[${i}].x), c.a);`,
},
hueShift: {
kind: 'op', label: 'hue shift', desc: 'rotar matiz',
params: [
{ k: 'h', label: 'h', min: 0, max: 1, d: 0, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
let k = vec3<f32>(0.57735);
let ca = cos(p.x * 6.2832);
let sa = sin(p.x * 6.2832);
let rot = c.rgb * ca + cross(k, c.rgb) * sa + k * dot(k, c.rgb) * (1.0 - ca);
return vec4<f32>(rot, c.a);`,
},
tint: {
kind: 'op', label: 'tint', desc: 'rgb × tinte',
params: [
{ k: 'r', label: 'r', min: 0, max: 2, d: 1, step: 0.01 },
{ k: 'g', label: 'g', min: 0, max: 2, d: 1, step: 0.01 },
{ k: 'b', label: 'b', min: 0, max: 2, d: 1, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
return vec4<f32>(c.rgb * vec3<f32>(p.x, p.y, p.z), c.a);`,
},
posterize: {
kind: 'op', label: 'posterize', desc: 'cuantizar a N niveles',
params: [
{ k: 'levels', label: 'niveles', min: 2, max: 16, d: 5, step: 1 },
],
body: (i) => `
let n = max(2.0, u.params[${i}].x);
return vec4<f32>(floor(c.rgb * n) / n, c.a);`,
},
vignette: {
kind: 'op', label: 'vignette', desc: 'oscurecer bordes',
params: [
{ k: 'strength', label: 'fuerza', min: 0, max: 2, d: 1, step: 0.01 },
{ k: 'radius', label: 'radio', min: 0, max: 1.4, d: 0.5, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
let d = length(uv - vec2<f32>(0.5));
let v = 1.0 - smoothstep(p.y, p.y + 0.3, d) * p.x;
return vec4<f32>(c.rgb * v, c.a);`,
},
ripple: {
kind: 'op', label: 'ripple', desc: 'modular brillo con ondas',
params: [
{ k: 'freq', label: 'frecuencia', min: 1, max: 100, d: 30, step: 1 },
{ k: 'amp', label: 'amplitud', min: 0, max: 1, d: 0.2, step: 0.01 },
{ k: 'speed', label: 'velocidad', min: -5, max: 5, d: 2, step: 0.05 },
],
body: (i) => `
let p = u.params[${i}];
let d = length(uv - vec2<f32>(0.5));
let w = sin(d * p.x - u.time * p.z) * p.y;
return vec4<f32>(c.rgb * (1.0 + w), c.a);`,
},
pulse: {
kind: 'op', label: 'pulse', desc: 'multiplicar por onda',
params: [
{ k: 'freq', label: 'frecuencia', min: 0, max: 10, d: 2, step: 0.05 },
{ k: 'amount', label: 'cantidad', min: 0, max: 1, d: 0.3, step: 0.01 },
],
body: (i) => `
let p = u.params[${i}];
return vec4<f32>(c.rgb * (1.0 + p.y * sin(u.time * p.x)), c.a);`,
},
};
const NODES_BY_KIND = {
gen: Object.entries(NODES).filter(([, v]) => v.kind === 'gen').map(([k, v]) => ({ name: k, ...v })),
op: Object.entries(NODES).filter(([, v]) => v.kind === 'op' ).map(([k, v]) => ({ name: k, ...v })),
};
//
// Compilador: DAG WGSL
//
function compileDagToWGSL(pipeline) {
const safePipeline = pipeline.slice(0, MAX_NODES);
const fns = safePipeline.map((step, idx) => {
const def = NODES[step.name];
return `fn node_${idx}(c: vec4<f32>, uv: vec2<f32>) -> vec4<f32> {${def.body(idx)}
}`;
}).join('\n\n');
const chain = safePipeline.length === 0
? ' // pipeline vacío · fondo por defecto\n c = vec4<f32>(0.04, 0.04, 0.06, 1.0);'
: safePipeline.map((_, idx) => ` c = node_${idx}(c, uv);`).join('\n');
return `struct Uniforms {
time: f32,
_pad: f32,
resolution: vec2<f32>,
params: array<vec4<f32>, ${MAX_NODES}>,
};
@group(0) @binding(0) var<uniform> u: Uniforms;
@vertex
fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
let p = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0)
);
return vec4<f32>(p[i], 0.0, 1.0);
}
${fns}
@fragment
fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
let uv = pos.xy / u.resolution;
var c = vec4<f32>(0.0, 0.0, 0.0, 1.0);
${chain}
return c;
}
`;
}
//
// Uniform buffer: escribe los valores actuales de params + time/res
//
const UNIFORM_FLOATS = 4 + MAX_NODES * 4; // header (4) + params (MAX_NODES × 4)
const UNIFORM_BYTES = UNIFORM_FLOATS * 4; // 272 bytes
function writeUniforms(device, buffer, time, width, height, pipeline) {
const data = new Float32Array(UNIFORM_FLOATS);
data[0] = time;
data[1] = 0;
data[2] = width;
data[3] = height;
for (let i = 0; i < Math.min(pipeline.length, MAX_NODES); i++) {
const step = pipeline[i];
const def = NODES[step.name];
const offset = 4 + i * 4;
for (let j = 0; j < 4; j++) {
const p = def.params[j];
data[offset + j] = p ? (step.params[p.k] ?? p.d) : 0;
}
}
device.queue.writeBuffer(buffer, 0, data);
}
//
// Hook WebGPU + compilación de DAG
//
function useWebGPUDag(canvasRef, pipeline, topologyKey) {
const gpu = useRef({
device: null, context: null, format: null,
pipeline: null, bindGroup: null, uniformBuffer: null,
startTime: 0,
});
const pipelineRef = useRef(pipeline);
useEffect(() => { pipelineRef.current = pipeline; }, [pipeline]);
const [status, setStatus] = useState('init');
const [shaderError, setShaderError] = useState(null);
const [fps, setFps] = useState(0);
// Init GPU
useEffect(() => {
let cancelled = false;
(async () => {
try {
if (!navigator.gpu) { setStatus('unsupported'); return; }
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { setStatus('unsupported'); return; }
const device = await adapter.requestDevice();
if (cancelled) return;
const canvas = canvasRef.current;
if (!canvas) return;
canvas.width = 400; canvas.height = 400;
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format, alphaMode: 'premultiplied' });
const uniformBuffer = device.createBuffer({
size: UNIFORM_BYTES,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
gpu.current = {
...gpu.current, device, context, format, uniformBuffer,
startTime: performance.now(),
};
device.lost.then(() => setStatus('error'));
setStatus('ready');
} catch (e) {
console.error(e);
setShaderError(String(e.message || e));
setStatus('error');
}
})();
return () => { cancelled = true; };
}, [canvasRef]);
// Recompilar shader (solo en cambios de topología)
const compileShader = useCallback(async (wgsl) => {
const { device, format, uniformBuffer } = gpu.current;
if (!device) return;
device.pushErrorScope('validation');
const module = device.createShaderModule({ code: wgsl });
const info = await module.getCompilationInfo();
const errors = info.messages.filter(m => m.type === 'error');
if (errors.length > 0) {
setShaderError(errors.map(m => `línea ${m.lineNum}: ${m.message}`).join('\n'));
await device.popErrorScope();
return;
}
try {
const p = device.createRenderPipeline({
layout: 'auto',
vertex: { module, entryPoint: 'vs' },
fragment: { module, entryPoint: 'fs', targets: [{ format }] },
primitive:{ topology: 'triangle-list' },
});
const bg = device.createBindGroup({
layout: p.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: uniformBuffer } }],
});
gpu.current.pipeline = p;
gpu.current.bindGroup = bg;
setShaderError(null);
} catch (e) {
setShaderError(String(e.message || e));
}
const err = await device.popErrorScope();
if (err) setShaderError(err.message);
}, []);
useEffect(() => {
if (status !== 'ready') return;
const wgsl = compileDagToWGSL(pipelineRef.current);
compileShader(wgsl);
}, [topologyKey, status, compileShader]);
// Render loop (lee params actuales cada frame via ref)
useEffect(() => {
if (status !== 'ready') return;
let running = true;
let frames = 0;
let lastFpsSample = performance.now();
const loop = () => {
if (!running) return;
const { device, context, pipeline: gpuPipe, bindGroup, uniformBuffer, startTime } = gpu.current;
const canvas = canvasRef.current;
if (device && context && gpuPipe && bindGroup && canvas) {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));
const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w; canvas.height = h;
}
const t = (performance.now() - startTime) / 1000;
writeUniforms(device, uniformBuffer, t, canvas.width, canvas.height, pipelineRef.current);
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0, g: 0, b: 0, a: 1 },
loadOp: 'clear', storeOp: 'store',
}],
});
pass.setPipeline(gpuPipe);
pass.setBindGroup(0, bindGroup);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);
frames++;
const now = performance.now();
if (now - lastFpsSample >= 500) {
setFps(Math.round((frames * 1000) / (now - lastFpsSample)));
frames = 0; lastFpsSample = now;
}
}
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
return () => { running = false; };
}, [status, canvasRef]);
const resetTime = useCallback(() => {
gpu.current.startTime = performance.now();
}, []);
return { status, shaderError, fps, resetTime };
}
//
// Utilidades
//
let _uid = 0;
const uid = () => `n${++_uid}_${Date.now().toString(36)}`;
function defaultParams(name) {
const d = NODES[name];
return Object.fromEntries((d.params || []).map(p => [p.k, p.d]));
}
//
// Sub-componentes
//
function PaletteItem({ node, color, onDragStart, onClick }) {
return (
<div
draggable
onDragStart={(e) => {
e.dataTransfer.setData('text/node-name', node.name);
e.dataTransfer.effectAllowed = 'copy';
onDragStart?.();
}}
onClick={onClick}
className="group flex items-center gap-2 px-2 py-1 rounded cursor-grab active:cursor-grabbing select-none transition-colors hover:bg-neutral-800/60"
style={{ borderLeft: `2px solid ${color}` }}
title={node.desc}
>
<span className="font-mono text-[11px] font-medium flex-1" style={{ color }}>{node.label}</span>
<span className="font-mono text-[8px] text-neutral-600 opacity-0 group-hover:opacity-100 transition-opacity uppercase tracking-wider">drag</span>
</div>
);
}
function ParamSlider({ param, value, onChange, color }) {
const display = param.step >= 1 ? Math.round(value) : Number(value).toFixed(param.step < 0.1 ? 3 : 2);
return (
<div className="flex items-center gap-2">
<span className="font-mono text-[10px] text-neutral-400 w-14 shrink-0">{param.label}</span>
<input
type="range"
min={param.min} max={param.max} step={param.step}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
className="flex-1"
style={{ accentColor: color, minWidth: '60px' }}
/>
<span className="font-mono text-[10px] text-neutral-500 w-10 text-right tabular-nums shrink-0">{display}</span>
</div>
);
}
function PipelineNode({ step, index, onRemove, onParamChange, onDragStart, onDrop, isDragging }) {
const def = NODES[step.name];
const color = def.kind === 'gen' ? GEN_COLOR : OP_COLOR;
return (
<div
draggable
onDragStart={(e) => {
e.dataTransfer.setData('text/reorder-index', String(index));
e.dataTransfer.effectAllowed = 'move';
onDragStart?.(index);
}}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => onDrop?.(e, index)}
className="rounded-lg transition-all"
style={{
background: `linear-gradient(180deg, ${color}14 0%, ${color}06 100%)`,
border: `1px solid ${color}30`,
opacity: isDragging ? 0.4 : 1,
}}
>
<div className="flex items-center gap-2 px-2.5 py-1.5 border-b" style={{ borderColor: `${color}20` }}>
<GripVertical size={11} className="text-neutral-600 cursor-grab shrink-0" />
<span className="font-mono text-[9px] uppercase tracking-wider text-neutral-500 shrink-0">
{def.kind} · {index}
</span>
<span className="font-mono text-xs font-semibold flex-1 truncate" style={{ color }}>{def.label}</span>
<button
onClick={() => onRemove(index)}
className="text-neutral-500 hover:text-neutral-200 p-0.5"
aria-label="Eliminar"
>
<X size={11} />
</button>
</div>
{def.params.length > 0 && (
<div className="px-2.5 py-2 flex flex-col gap-1.5">
{def.params.map(p => (
<ParamSlider
key={p.k}
param={p}
value={step.params[p.k] ?? p.d}
onChange={(v) => onParamChange(index, p.k, v)}
color={color}
/>
))}
</div>
)}
{def.params.length === 0 && (
<div className="px-2.5 py-1.5 font-mono text-[9px] text-neutral-600 italic">sin parámetros</div>
)}
</div>
);
}
function StatusBadge({ status }) {
const map = {
init: { color: '#fbbf24', label: 'inicializando' },
ready: { color: ACCENT, label: 'activo' },
unsupported: { color: '#f43f5e', label: 'sin webgpu' },
error: { color: '#f43f5e', label: 'error' },
};
const s = map[status] || map.init;
return (
<span className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full" style={{ background: s.color, boxShadow: `0 0 8px ${s.color}` }} />
<span className="font-mono text-[10px]" style={{ color: s.color }}>{s.label}</span>
</span>
);
}
function StatusOverlay({ status, error }) {
if (status === 'ready') return null;
return (
<div className="absolute inset-0 flex items-center justify-center p-6 text-center" style={{ background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(4px)' }}>
{status === 'init' && <div className="font-mono text-[11px] text-neutral-400">inicializando adaptador GPU</div>}
{status === 'unsupported' && (
<div className="max-w-sm">
<div className="font-display text-xl italic mb-2" style={{ color: '#f43f5e' }}>WebGPU no disponible</div>
<div className="font-mono text-[10px] text-neutral-400 leading-relaxed">
chrome/edge 113+, safari 18+, o firefox nightly con flag. si estás en un navegador compatible, prueba abrir el artifact en pestaña nueva.
</div>
</div>
)}
{status === 'error' && (
<div className="max-w-sm">
<div className="font-display text-xl italic mb-2" style={{ color: '#f43f5e' }}>error</div>
<div className="font-mono text-[10px] text-neutral-400 whitespace-pre-wrap">{error}</div>
</div>
)}
</div>
);
}
//
// APP
//
export default function App() {
const canvasRef = useRef(null);
const [pipeline, setPipeline] = useState(() => [
{ id: uid(), name: 'plasma', params: defaultParams('plasma') },
{ id: uid(), name: 'vignette', params: defaultParams('vignette') },
]);
const [dragIndex, setDragIndex] = useState(null);
const [wgslOpen, setWgslOpen] = useState(false);
// La clave de topología cambia SOLO cuando cambia la estructura (no params)
const topologyKey = useMemo(() => pipeline.map(s => s.name).join('|'), [pipeline]);
const { status, shaderError, fps, resetTime } = useWebGPUDag(canvasRef, pipeline, topologyKey);
const generatedWGSL = useMemo(() => compileDagToWGSL(pipeline), [topologyKey]);
const addNode = (name) => {
if (pipeline.length >= MAX_NODES) return;
setPipeline(p => [...p, { id: uid(), name, params: defaultParams(name) }]);
};
const removeNode = (idx) => setPipeline(p => p.filter((_, i) => i !== idx));
const changeParam = (idx, key, value) => {
setPipeline(p => p.map((s, i) => i === idx ? { ...s, params: { ...s.params, [key]: value } } : s));
};
const handleDropOnZone = (e) => {
e.preventDefault();
const nodeName = e.dataTransfer.getData('text/node-name');
const reorderIdx = e.dataTransfer.getData('text/reorder-index');
if (nodeName && NODES[nodeName]) {
addNode(nodeName);
} else if (reorderIdx !== '') {
const from = Number(reorderIdx);
setPipeline(p => {
const copy = [...p];
const [m] = copy.splice(from, 1);
copy.push(m);
return copy;
});
}
setDragIndex(null);
};
const handleDropOnNode = (e, targetIdx) => {
e.preventDefault();
e.stopPropagation();
const nodeName = e.dataTransfer.getData('text/node-name');
const reorderIdx = e.dataTransfer.getData('text/reorder-index');
if (nodeName && NODES[nodeName]) {
setPipeline(p => {
const copy = [...p];
copy.splice(targetIdx, 0, { id: uid(), name: nodeName, params: defaultParams(nodeName) });
return copy;
});
} else if (reorderIdx !== '') {
const from = Number(reorderIdx);
if (from === targetIdx) return;
setPipeline(p => {
const copy = [...p];
const [m] = copy.splice(from, 1);
const adj = from < targetIdx ? targetIdx - 1 : targetIdx;
copy.splice(adj, 0, m);
return copy;
});
}
setDragIndex(null);
};
return (
<div className="w-full min-h-screen text-neutral-200" style={{
background: 'radial-gradient(ellipse at top, #0f1419 0%, #06080a 60%, #030506 100%)',
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
}}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
.font-display { font-family: 'Fraunces', serif; font-optical-sizing: auto; }
.font-mono { font-family: 'JetBrains Mono', ui-monospace, monospace; }
.grid-bg {
background-image:
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
background-size: 24px 24px;
}
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.16); }
input[type="range"] { height: 4px; }
`}</style>
<div className="grid-bg min-h-screen">
{/* HEADER */}
<header className="px-6 pt-6 pb-5 border-b border-white/5">
<div className="flex items-end justify-between flex-wrap gap-4">
<div>
<div className="flex items-baseline gap-3">
<span className="font-mono text-[10px] uppercase tracking-[0.3em] text-neutral-500">lab · 003</span>
<span className="font-mono text-[10px] text-neutral-700">/</span>
<span className="font-mono text-[10px] text-neutral-500">shader dag · webgpu</span>
<span className="font-mono text-[10px] text-neutral-700">/</span>
<StatusBadge status={status} />
</div>
<h1 className="font-display text-3xl font-light tracking-tight mt-1.5">
composición de <em className="italic" style={{ color: ACCENT }}>fragmentos</em>
</h1>
<p className="font-mono text-[11px] text-neutral-500 mt-1.5 max-w-2xl">
cada nodo emite un snippet WGSL · el DAG se concatena en un único fragment shader · los sliders actualizan uniforms sin recompilar
</p>
</div>
<div className="flex items-center gap-3">
<span className="font-mono text-[10px] text-neutral-600">{fps} fps</span>
<button
onClick={resetTime}
className="flex items-center gap-1.5 font-mono text-[10px] uppercase tracking-wider text-neutral-500 hover:text-neutral-200 border border-white/10 hover:border-white/30 px-3 py-1.5 rounded transition-colors"
>
<RotateCcw size={11} /> reset t
</button>
<button
onClick={() => setPipeline([])}
className="flex items-center gap-1.5 font-mono text-[10px] uppercase tracking-wider text-neutral-500 hover:text-neutral-200 border border-white/10 hover:border-white/30 px-3 py-1.5 rounded transition-colors"
>
<Trash2 size={11} /> vaciar
</button>
</div>
</div>
</header>
{/* MAIN · 3 columnas: paleta | pipeline+sliders | canvas+wgsl */}
<main style={{
display: 'grid',
gridTemplateColumns: '180px minmax(280px, 1fr) minmax(320px, 420px)',
minHeight: 'calc(100vh - 110px)',
overflowX: 'auto',
}}>
{/* ── PALETA ── */}
<aside className="border-r border-white/5 p-4 overflow-y-auto" style={{ maxHeight: 'calc(100vh - 110px)' }}>
<div className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500 mb-4">primitivas</div>
<section className="mb-5">
<div className="flex items-center gap-2 mb-2 px-1">
<span className="w-1.5 h-1.5 rounded-full" style={{ background: GEN_COLOR, boxShadow: `0 0 8px ${GEN_COLOR}` }} />
<span className="font-display text-sm italic" style={{ color: GEN_COLOR }}>generadores</span>
</div>
<div className="font-mono text-[9px] text-neutral-600 mb-2 px-1">producen color · ignoran c</div>
<div className="flex flex-col gap-0.5">
{NODES_BY_KIND.gen.map(n => (
<PaletteItem key={n.name} node={n} color={GEN_COLOR} onClick={() => addNode(n.name)} />
))}
</div>
</section>
<section>
<div className="flex items-center gap-2 mb-2 px-1">
<span className="w-1.5 h-1.5 rounded-full" style={{ background: OP_COLOR, boxShadow: `0 0 8px ${OP_COLOR}` }} />
<span className="font-display text-sm italic" style={{ color: OP_COLOR }}>operadores</span>
</div>
<div className="font-mono text-[9px] text-neutral-600 mb-2 px-1">transforman c · punto a punto</div>
<div className="flex flex-col gap-0.5">
{NODES_BY_KIND.op.map(n => (
<PaletteItem key={n.name} node={n} color={OP_COLOR} onClick={() => addNode(n.name)} />
))}
</div>
</section>
<div className="mt-6 pt-4 border-t border-white/5 font-mono text-[9px] text-neutral-600 leading-relaxed">
tip · click o drag para añadir. reordena arrastrando nodos del pipeline.
</div>
</aside>
{/* ── PIPELINE (vertical, con sliders integrados) ── */}
<section className="p-4 overflow-y-auto border-r border-white/5" style={{ maxHeight: 'calc(100vh - 110px)' }}>
<div className="flex items-baseline gap-3 mb-3">
<span className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500">pipeline</span>
<span className="font-mono text-[10px] text-neutral-600">
{pipeline.length}/{MAX_NODES} nodos
</span>
</div>
<div
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; }}
onDrop={handleDropOnZone}
className="rounded-xl border border-dashed border-white/10 p-3 transition-colors hover:border-white/20"
style={{ background: 'rgba(255,255,255,0.015)', minHeight: '300px' }}
>
{pipeline.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="font-display text-lg italic text-neutral-500 mb-1">pipeline vacío</div>
<div className="font-mono text-[10px] text-neutral-600">arrastra una primitiva de la izquierda</div>
</div>
) : (
<div className="flex flex-col gap-2">
{pipeline.map((step, i) => (
<PipelineNode
key={step.id}
step={step}
index={i}
onRemove={removeNode}
onParamChange={changeParam}
onDragStart={setDragIndex}
onDrop={handleDropOnNode}
isDragging={dragIndex === i}
/>
))}
</div>
)}
</div>
<div className="mt-4 font-mono text-[9px] text-neutral-600 leading-relaxed border-l-2 border-neutral-800 pl-3">
<span style={{ color: GEN_COLOR }}>gen</span> produce · <span style={{ color: OP_COLOR }}>op</span> transforma · el flujo es c node₀ node₁ nodeₙ
</div>
</section>
{/* ── CANVAS + WGSL ── */}
<aside className="p-4 overflow-y-auto flex flex-col gap-3" style={{ maxHeight: 'calc(100vh - 110px)' }}>
<div className="flex items-baseline gap-3">
<span className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500">output</span>
<span className="font-mono text-[9px] text-neutral-600">fragment · fullscreen triangle</span>
</div>
<div
className="rounded-xl border border-white/10 overflow-hidden relative"
style={{
aspectRatio: '1/1',
background: '#000',
boxShadow: `0 0 0 1px ${ACCENT}10, 0 20px 60px -30px ${ACCENT}30`,
}}
>
<canvas ref={canvasRef} className="block" style={{ width: '100%', height: '100%' }} />
{status !== 'ready' && <StatusOverlay status={status} error={shaderError} />}
</div>
{shaderError && status === 'ready' && (
<div className="rounded-lg p-3" style={{ background: '#f43f5e0a', border: '1px solid #f43f5e30' }}>
<div className="font-mono text-[10px] uppercase tracking-[0.2em] text-rose-400 mb-1.5 flex items-center gap-1.5">
<AlertCircle size={11} /> compilación fallida
</div>
<pre className="font-mono text-[10px] text-rose-300 whitespace-pre-wrap leading-relaxed">{shaderError}</pre>
<div className="font-mono text-[9px] text-neutral-500 mt-2">último pipeline válido sigue activo</div>
</div>
)}
{/* WGSL viewer */}
<div className="rounded-lg border border-white/5" style={{ background: 'rgba(255,255,255,0.015)' }}>
<button
onClick={() => setWgslOpen(o => !o)}
className="w-full flex items-center gap-2 px-3 py-2 font-mono text-[10px] uppercase tracking-[0.2em] text-neutral-500 hover:text-neutral-300"
>
{wgslOpen ? <ChevronDown size={11} /> : <ChevronRight size={11} />}
wgsl generado
<span className="ml-auto text-neutral-600 normal-case tracking-normal">{generatedWGSL.split('\n').length} líneas</span>
</button>
{wgslOpen && (
<pre className="font-mono text-[10px] text-neutral-400 px-3 pb-3 overflow-auto leading-relaxed" style={{ maxHeight: '260px' }}>
{generatedWGSL}
</pre>
)}
</div>
<div className="font-mono text-[9px] text-neutral-600 leading-relaxed border-l-2 border-neutral-800 pl-3">
los sliders escriben a <span style={{ color: ACCENT }}>u.params[idx]</span> sin recompilar · solo cambios de topología regeneran WGSL
</div>
</aside>
</main>
</div>
</div>
);
}
@@ -1,505 +0,0 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { Play, RotateCcw, AlertCircle } from 'lucide-react';
//
// Presets WGSL · cada uno es un shader completo autosuficiente
//
const SHADER_HEADER = `struct Uniforms {
time: f32,
resolution: vec2<f32>,
};
@group(0) @binding(0) var<uniform> u: Uniforms;
@vertex
fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
// triángulo fullscreen (truco de tres vértices)
let p = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0)
);
return vec4<f32>(p[i], 0.0, 1.0);
}
`;
const PRESETS = {
plasma: SHADER_HEADER + `
@fragment
fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
let uv = pos.xy / u.resolution;
let c = 0.5 + 0.5 * cos(u.time + uv.xyx + vec3<f32>(0.0, 2.0, 4.0));
return vec4<f32>(c, 1.0);
}
`,
circle: SHADER_HEADER + `
@fragment
fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
// SDF de un círculo con anillo y fondo sutil
let uv = (pos.xy / u.resolution) * 2.0 - 1.0;
let aspect = u.resolution.x / u.resolution.y;
let p = vec2<f32>(uv.x * aspect, uv.y);
let r = 0.4 + 0.05 * sin(u.time * 2.0);
let d = length(p) - r;
let fill = smoothstep(0.0, -0.01, d);
let ring = smoothstep(0.015, 0.0, abs(d));
let bg = vec3<f32>(0.05, 0.06, 0.08);
let col = mix(bg, vec3<f32>(0.94, 0.55, 0.72), fill) + vec3<f32>(ring * 0.9);
return vec4<f32>(col, 1.0);
}
`,
checker: SHADER_HEADER + `
@fragment
fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
// tablero rotando
let uv = pos.xy / u.resolution - 0.5;
let aspect = u.resolution.x / u.resolution.y;
let p0 = vec2<f32>(uv.x * aspect, uv.y);
let rot = u.time * 0.25;
let c = cos(rot);
let s = sin(rot);
let p = vec2<f32>(p0.x * c - p0.y * s, p0.x * s + p0.y * c) * 10.0;
let chk = (floor(p.x) + floor(p.y)) - 2.0 * floor((floor(p.x) + floor(p.y)) * 0.5);
let col = mix(vec3<f32>(0.93, 0.91, 0.86), vec3<f32>(0.10, 0.09, 0.13), chk);
return vec4<f32>(col, 1.0);
}
`,
waves: SHADER_HEADER + `
@fragment
fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
// interferencia de dos ondas senoidales
let uv = pos.xy / u.resolution;
let a = sin(uv.x * 20.0 + u.time * 2.0);
let b = sin(uv.y * 15.0 - u.time * 1.3);
let v = 0.5 + 0.5 * a * b;
let col = vec3<f32>(v, pow(v, 2.0) * 0.5 + 0.3, 1.0 - v * 0.7);
return vec4<f32>(col, 1.0);
}
`,
sdfBlob: SHADER_HEADER + `
fn sdCircle(p: vec2<f32>, r: f32) -> f32 { return length(p) - r; }
fn smoothMin(a: f32, b: f32, k: f32) -> f32 {
let h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * h * k * (1.0 / 6.0);
}
@fragment
fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
// metaball · fusión de tres SDFs
let uv = (pos.xy / u.resolution) * 2.0 - 1.0;
let aspect = u.resolution.x / u.resolution.y;
let p = vec2<f32>(uv.x * aspect, uv.y);
let t = u.time;
let c1 = sdCircle(p - vec2<f32>(cos(t) * 0.4, sin(t * 1.3) * 0.3), 0.25);
let c2 = sdCircle(p - vec2<f32>(cos(t * 0.7 + 2.0) * 0.35, sin(t * 0.9) * 0.25), 0.22);
let c3 = sdCircle(p - vec2<f32>(sin(t * 1.1) * 0.3, cos(t * 0.6) * 0.35), 0.2);
let d = smoothMin(smoothMin(c1, c2, 0.35), c3, 0.35);
let fill = smoothstep(0.02, -0.02, d);
let glow = exp(-8.0 * max(d, 0.0));
let bg = vec3<f32>(0.04, 0.02, 0.06);
let core = vec3<f32>(0.95, 0.4, 0.6);
let col = bg + core * (fill * 0.9 + glow * 0.3);
return vec4<f32>(col, 1.0);
}
`,
};
const PRESET_ORDER = [
{ key: 'plasma', label: 'plasma', hint: 'cos-gradient clásico' },
{ key: 'circle', label: 'círculo sdf', hint: 'signed distance field' },
{ key: 'checker', label: 'tablero', hint: 'patrón rotando' },
{ key: 'waves', label: 'ondas', hint: 'interferencia senoidal' },
{ key: 'sdfBlob', label: 'metaball', hint: 'fusión suave de SDFs' },
];
//
// Hook: gestión de todo el ciclo WebGPU
//
function useWebGPU(canvasRef, code) {
const gpu = useRef({
device: null,
context: null,
format: null,
pipeline: null,
bindGroup: null,
uniformBuffer: null,
startTime: 0,
raf: 0,
});
const [status, setStatus] = useState('init'); // init · ready · unsupported · error
const [shaderError, setShaderError] = useState(null);
const [fps, setFps] = useState(0);
const [timeT, setTimeT] = useState(0);
// Inicialización
useEffect(() => {
let cancelled = false;
(async () => {
try {
if (!navigator.gpu) { setStatus('unsupported'); return; }
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { setStatus('unsupported'); return; }
const device = await adapter.requestDevice();
if (cancelled) return;
const canvas = canvasRef.current;
if (!canvas) return;
canvas.width = 400;
canvas.height = 400;
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format, alphaMode: 'premultiplied' });
const uniformBuffer = device.createBuffer({
size: 16, // f32 time + pad + vec2<f32> resolution
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
gpu.current = {
...gpu.current,
device, context, format, uniformBuffer,
startTime: performance.now(),
};
device.lost.then(() => setStatus('error'));
setStatus('ready');
} catch (e) {
console.error(e);
setShaderError(String(e.message || e));
setStatus('error');
}
})();
return () => { cancelled = true; };
}, [canvasRef]);
// Compilación del shader (debounced)
const compileShader = useCallback(async (wgsl) => {
const { device, format, uniformBuffer } = gpu.current;
if (!device) return;
device.pushErrorScope('validation');
const module = device.createShaderModule({ code: wgsl });
const info = await module.getCompilationInfo();
const errors = info.messages.filter(m => m.type === 'error');
if (errors.length > 0) {
const msg = errors.map(m => `línea ${m.lineNum}: ${m.message}`).join('\n');
setShaderError(msg);
await device.popErrorScope();
return;
}
try {
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: { module, entryPoint: 'vs' },
fragment: { module, entryPoint: 'fs', targets: [{ format }] },
primitive:{ topology: 'triangle-list' },
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: uniformBuffer } }],
});
gpu.current.pipeline = pipeline;
gpu.current.bindGroup = bindGroup;
setShaderError(null);
} catch (e) {
setShaderError(String(e.message || e));
}
const err = await device.popErrorScope();
if (err) setShaderError(err.message);
}, []);
useEffect(() => {
if (status !== 'ready') return;
const id = setTimeout(() => compileShader(code), 180);
return () => clearTimeout(id);
}, [code, status, compileShader]);
// Render loop
useEffect(() => {
if (status !== 'ready') return;
let running = true;
let frames = 0;
let lastFpsSample = performance.now();
const loop = () => {
if (!running) return;
const { device, context, pipeline, bindGroup, uniformBuffer, startTime } = gpu.current;
const canvas = canvasRef.current;
if (device && context && pipeline && bindGroup && canvas) {
// redimensionado
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));
const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w;
canvas.height = h;
}
const t = (performance.now() - startTime) / 1000;
const data = new Float32Array([t, 0, canvas.width, canvas.height]);
device.queue.writeBuffer(uniformBuffer, 0, data);
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0, g: 0, b: 0, a: 1 },
loadOp: 'clear',
storeOp: 'store',
}],
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);
frames++;
const now = performance.now();
if (now - lastFpsSample >= 500) {
setFps(Math.round((frames * 1000) / (now - lastFpsSample)));
setTimeT(t);
frames = 0;
lastFpsSample = now;
}
}
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
return () => { running = false; };
}, [status, canvasRef]);
const resetTime = useCallback(() => {
gpu.current.startTime = performance.now();
}, []);
return { status, shaderError, fps, timeT, resetTime };
}
//
// APP
//
export default function App() {
const canvasRef = useRef(null);
const [code, setCode] = useState(PRESETS.plasma);
const [activePreset, setActivePreset] = useState('plasma');
const { status, shaderError, fps, timeT, resetTime } = useWebGPU(canvasRef, code);
const loadPreset = (key) => {
setActivePreset(key);
setCode(PRESETS[key]);
};
const ACCENT = '#5eead4'; // teal-300
return (
<div className="w-full min-h-screen text-neutral-200" style={{
background: 'radial-gradient(ellipse at top, #0f1419 0%, #06080a 60%, #030506 100%)',
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
}}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
.font-display { font-family: 'Fraunces', serif; font-optical-sizing: auto; }
.font-mono { font-family: 'JetBrains Mono', ui-monospace, monospace; }
.grid-bg {
background-image:
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
background-size: 24px 24px;
}
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.16); }
textarea.wgsl {
tab-size: 2;
-moz-tab-size: 2;
}
`}</style>
<div className="grid-bg min-h-screen">
{/* HEADER */}
<header className="px-6 pt-6 pb-5 border-b border-white/5">
<div className="flex items-end justify-between flex-wrap gap-4">
<div>
<div className="flex items-baseline gap-3">
<span className="font-mono text-[10px] uppercase tracking-[0.3em] text-neutral-500">lab · 002</span>
<span className="font-mono text-[10px] text-neutral-700">/</span>
<span className="font-mono text-[10px] text-neutral-500">shaders · webgpu</span>
<span className="font-mono text-[10px] text-neutral-700">/</span>
<StatusBadge status={status} />
</div>
<h1 className="font-display text-3xl font-light tracking-tight mt-1.5">
laboratorio de <em className="italic" style={{color: ACCENT}}>píxels</em>
</h1>
<p className="font-mono text-[11px] text-neutral-500 mt-1.5 max-w-2xl">
editor WGSL en vivo · cada tecla recompila el fragment shader · el uniform block expone <span style={{color: ACCENT}}>time</span> y <span style={{color: ACCENT}}>resolution</span>
</p>
</div>
<div className="flex items-center gap-3">
<span className="font-mono text-[10px] text-neutral-600">t = {timeT.toFixed(2)}s</span>
<span className="font-mono text-[10px] text-neutral-600">{fps} fps</span>
<button
onClick={resetTime}
className="flex items-center gap-1.5 font-mono text-[10px] uppercase tracking-wider text-neutral-500 hover:text-neutral-200 border border-white/10 hover:border-white/30 px-3 py-1.5 rounded transition-colors"
>
<RotateCcw size={11} /> reset t
</button>
</div>
</div>
</header>
{/* MAIN · 2 columnas: canvas | editor */}
<main style={{
display: 'grid',
gridTemplateColumns: 'minmax(280px, 1fr) minmax(340px, 1fr)',
minHeight: 'calc(100vh - 110px)',
overflowX: 'auto',
}}>
{/* IZQUIERDA · CANVAS */}
<section className="p-4 md:p-6 border-r border-white/5 flex flex-col gap-3" style={{maxHeight: 'calc(100vh - 110px)'}}>
<div className="flex items-baseline gap-3">
<span className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500">canvas</span>
<span className="font-mono text-[9px] text-neutral-600">fragment shader · fullscreen triangle</span>
</div>
<div
className="flex-1 rounded-xl border border-white/10 overflow-hidden relative"
style={{
background: '#000',
boxShadow: `0 0 0 1px ${ACCENT}10, 0 20px 60px -30px ${ACCENT}30`,
minHeight: '320px',
}}
>
<canvas
ref={canvasRef}
className="block"
style={{width: '100%', height: '100%'}}
/>
{status !== 'ready' && <StatusOverlay status={status} error={shaderError} />}
</div>
<div className="font-mono text-[9px] text-neutral-600">
consejo · el vertex shader genera un triángulo que cubre toda la pantalla. todo el trabajo interesante pasa en el <span style={{color: ACCENT}}>fragment</span>.
</div>
</section>
{/* DERECHA · EDITOR */}
<section className="p-4 md:p-6 flex flex-col gap-3" style={{maxHeight: 'calc(100vh - 110px)'}}>
{/* presets */}
<div>
<div className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500 mb-2">presets</div>
<div className="flex flex-wrap gap-1.5">
{PRESET_ORDER.map(p => {
const active = p.key === activePreset;
return (
<button
key={p.key}
onClick={() => loadPreset(p.key)}
className="font-mono text-[11px] px-2.5 py-1 rounded transition-all"
style={{
background: active ? `${ACCENT}18` : 'rgba(255,255,255,0.02)',
border: `1px solid ${active ? ACCENT + '60' : 'rgba(255,255,255,0.08)'}`,
color: active ? ACCENT : '#a3a3a3',
}}
title={p.hint}
>
{p.label}
</button>
);
})}
</div>
</div>
{/* editor */}
<div className="flex flex-col flex-1 gap-2 min-h-0">
<div className="flex items-baseline gap-3">
<span className="font-mono text-[10px] uppercase tracking-[0.25em] text-neutral-500">fuente · wgsl</span>
{shaderError ? (
<span className="font-mono text-[10px] text-rose-400 flex items-center gap-1">
<AlertCircle size={10} /> error de compilación
</span>
) : (
<span className="font-mono text-[10px]" style={{color: ACCENT}}> compilado</span>
)}
</div>
<textarea
className="wgsl flex-1 w-full rounded-lg p-3 font-mono text-[12px] leading-relaxed resize-none outline-none"
value={code}
onChange={(e) => setCode(e.target.value)}
spellCheck={false}
style={{
background: 'rgba(255,255,255,0.02)',
border: `1px solid ${shaderError ? '#f43f5e40' : 'rgba(255,255,255,0.08)'}`,
color: '#d4d4d4',
minHeight: '300px',
}}
/>
</div>
{/* errores */}
{shaderError && (
<div className="rounded-lg p-3" style={{
background: '#f43f5e0a',
border: '1px solid #f43f5e30',
}}>
<div className="font-mono text-[10px] uppercase tracking-[0.2em] text-rose-400 mb-1.5 flex items-center gap-1.5">
<AlertCircle size={11} /> compilación fallida
</div>
<pre className="font-mono text-[11px] text-rose-300 whitespace-pre-wrap leading-relaxed">{shaderError}</pre>
</div>
)}
<div className="font-mono text-[9px] text-neutral-600 leading-relaxed border-l-2 border-neutral-800 pl-3">
uniforms · <span style={{color: ACCENT}}>u.time</span> (f32, segundos desde inicio) · <span style={{color: ACCENT}}>u.resolution</span> (vec2&lt;f32&gt;, px). el último pipeline válido se mantiene hasta que la próxima compilación tenga éxito.
</div>
</section>
</main>
</div>
</div>
);
}
//
// Sub-componentes
//
function StatusBadge({ status }) {
const map = {
init: { color: '#fbbf24', label: 'inicializando' },
ready: { color: '#5eead4', label: 'activo' },
unsupported: { color: '#f43f5e', label: 'sin webgpu' },
error: { color: '#f43f5e', label: 'error' },
};
const s = map[status] || map.init;
return (
<span className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full" style={{background: s.color, boxShadow: `0 0 8px ${s.color}`}} />
<span className="font-mono text-[10px]" style={{color: s.color}}>{s.label}</span>
</span>
);
}
function StatusOverlay({ status, error }) {
if (status === 'ready') return null;
return (
<div className="absolute inset-0 flex items-center justify-center p-6 text-center" style={{background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(4px)'}}>
{status === 'init' && (
<div className="font-mono text-[11px] text-neutral-400">inicializando adaptador GPU</div>
)}
{status === 'unsupported' && (
<div className="max-w-sm">
<div className="font-display text-xl italic mb-2" style={{color: '#f43f5e'}}>WebGPU no disponible</div>
<div className="font-mono text-[10px] text-neutral-400 leading-relaxed">
este navegador no expone <code>navigator.gpu</code>. prueba con chrome/edge recientes, o safari 18+, o activa el flag en firefox nightly.
</div>
</div>
)}
{status === 'error' && (
<div className="max-w-sm">
<div className="font-display text-xl italic mb-2" style={{color: '#f43f5e'}}>error de inicialización</div>
<div className="font-mono text-[10px] text-neutral-400 whitespace-pre-wrap">{error}</div>
</div>
)}
</div>
);
}
-430
View File
@@ -1,430 +0,0 @@
#include "app_base.h"
#include "imgui.h"
#include "gfx/shader_canvas.h"
#include "gfx/gl_shader.h"
#include "gfx/uniform_parser.h"
#include "gfx/uniform_panel.h"
#include "gfx/dag_catalog.h"
#include "gfx/dag_compile.h"
#include "gfx/dag_uniforms.h"
#include "gfx/dag_panel.h"
#include "gfx/dag_node_editor.h"
#include "gfx/dag_palette.h"
#include "gfx/dag_node_previews.h"
#include "gfx/code_to_generator.h"
#include "gfx/shaderlab_db.h"
#include "core/panel_menu.h"
#include "core/layouts_menu.h"
#include "core/layout_storage.h"
#include "core/modal_dialog.h"
#include "core/text_input.h"
#include "core/button.h"
#include "core/tokens.h"
#include "compiler.h"
#include <chrono>
#include <cctype>
#include <cstring>
#include <string>
#include <utility>
#include <vector>
// Globals: linked extern desde compiler.cpp. NO `static` aqui.
fn::gfx::ShaderCanvas g_canvas_code;
fn::gfx::ShaderCanvas g_canvas_dag;
// Default placeholder so the Code panel does something useful on first launch
// without committing to one specific look.
static const char* CODE_PLACEHOLDER = R"glsl(// Escribe tu fragment shader aqui.
// Declara uniforms con anotaciones (// @slider, @color, @xy)
// para que aparezcan como controles al guardar como generator.
uniform vec3 u_color; // @color default=0.5,0.2,0.8
uniform float u_speed; // @slider min=0.1 max=5 default=1
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
float t = u_time * u_speed;
vec3 c = u_color * (0.5 + 0.5 * cos(t + uv.xyx + vec3(0.0, 2.0, 4.0)));
fragColor = vec4(c, 1.0);
}
)glsl";
std::string g_source = CODE_PLACEHOLDER;
std::string g_code_err;
int g_code_err_line = -1;
std::chrono::steady_clock::time_point g_code_last_edit;
bool g_code_dirty = true;
std::vector<fn::gfx::UniformDescriptor> g_descs;
fn::gfx::UniformStore g_store;
std::vector<fn::gfx::DagStep> g_pipeline;
std::string g_dag_glsl;
std::string g_dag_err;
int g_dag_err_line = -1;
static bool g_dag_dirty = true; // solo lo usa main.cpp
// ── Panel visibility (toggled from View menu and panel close button) ──────
static bool g_show_code = true;
static bool g_show_dag = true;
static bool g_show_canvas_c = true;
static bool g_show_canvas_d = true;
static bool g_show_controls = true;
static bool g_show_functions = true;
static bool g_show_generated = true;
// Tabla de paneles toggleables que fn::run_app pasa a app_menubar cada frame.
// Vive en el ambito del archivo para poder referenciarse desde main().
static constexpr fn_ui::PanelToggle k_panels[] = {
{"Code", "Ctrl+1", &g_show_code},
{"DAG Pipeline", "Ctrl+2", &g_show_dag},
{"Canvas Code", "Ctrl+3", &g_show_canvas_c},
{"Canvas DAG", "Ctrl+4", &g_show_canvas_d},
{"Controls", "Ctrl+5", &g_show_controls},
{"Functions", "Ctrl+6", &g_show_functions},
{"Generated GLSL","Ctrl+7", &g_show_generated},
};
// ── Layouts (named ImGui ini snapshots persisted in shaders_lab.db) ───────
// El storage opaco encapsula la BD y el blob pendiente. Los callbacks
// envuelven save/apply/delete/reset y se pasan a app_menubar tal cual.
static fn_ui::LayoutStorage* g_layouts = nullptr;
static fn_ui::LayoutCallbacks g_layout_cb;
// ── Save-as-generator modal state ─────────────────────────────────────────
static bool g_save_modal_open = false;
static char g_save_name[64] = "my_shader";
static char g_save_label[64] = "my shader";
static char g_save_desc[256] = "";
static char g_save_tags[128] = "shaders_lab,user";
static std::string g_save_err;
// compile_code, compile_dag, mark_code_dirty viven en compiler.cpp
using shaders_lab::compile_code;
using shaders_lab::compile_dag;
using shaders_lab::mark_code_dirty;
static void ensure_dag_default() {
if (g_pipeline.empty()) {
const fn::gfx::DagNodeDef* plasma = fn::gfx::dag_find("plasma");
if (plasma) {
fn::gfx::DagStep s;
s.id = "n_plasma";
s.name = plasma->name;
s.params = plasma->param_defaults;
g_pipeline.push_back(s);
}
}
bool has_output = false;
for (const auto& s : g_pipeline) {
const fn::gfx::DagNodeDef* d = fn::gfx::dag_find(s.name);
if (d && d->kind == fn::gfx::DagKind::Output) { has_output = true; break; }
}
if (!has_output) {
const fn::gfx::DagNodeDef* out = fn::gfx::dag_find("output");
if (out) {
fn::gfx::DagStep s;
s.id = "n_output";
s.name = out->name;
s.editor_pos_x = 500.0f;
if (!g_pipeline.empty()) s.source_ids[0] = g_pipeline.front().id;
g_pipeline.push_back(s);
}
}
}
static void draw_err(const std::string& msg, int line) {
if (msg.empty()) return;
ImGui::Separator();
if (line > 0) {
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "line %d: %s", line, msg.c_str());
} else {
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", msg.c_str());
}
}
// snake_case validation: lowercase letters, digits, underscores; first char a-z.
static bool valid_id(const char* s) {
if (!s || !*s) return false;
if (!(*s >= 'a' && *s <= 'z')) return false;
for (const char* p = s; *p; ++p) {
char c = *p;
if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_')) return false;
}
return true;
}
// Build a DagNodeDef from current Code source + form fields, persist it, and
// register in the live catalog. Returns "" on success or an error message.
static std::string save_current_as_generator() {
if (!valid_id(g_save_name)) return "name must be snake_case (a-z, 0-9, _) and start with a letter";
if (fn::gfx::dag_find(g_save_name)) {
const fn::gfx::DagNodeDef* existing = fn::gfx::dag_find(g_save_name);
if (existing && existing->is_builtin) {
return std::string("name '") + g_save_name + "' collides with a built-in node";
}
// user node with same name → overwrite is allowed
}
auto tr = fn::gfx::code_to_generator(g_source);
if (!tr.ok) return tr.err;
fn::gfx::GeneratorRecord rec;
rec.id = g_save_name;
rec.label = g_save_label[0] ? g_save_label : g_save_name;
rec.description = g_save_desc;
rec.source_glsl = g_source;
rec.body_glsl = tr.body_template;
rec.param_count = tr.param_count;
rec.param_defaults = tr.param_defaults;
rec.param_names = tr.param_names;
rec.controls = tr.controls;
rec.tags = g_save_tags;
std::string err;
if (!fn::gfx::shaderlab_db_save_generator(rec, &err)) {
return std::string("db save failed: ") + err;
}
fn::gfx::DagNodeDef def = fn::gfx::make_generator_def(rec.id, rec.label, rec.description, tr);
if (!fn::gfx::dag_register_node(def)) {
return std::string("could not register node '") + rec.id + "'";
}
return "";
}
// Reconstitute every persisted generator and inject it into the live catalog.
static void load_user_generators_into_catalog() {
for (const auto& rec : fn::gfx::shaderlab_db_list_generators()) {
// Re-translate body_template from source to keep the lambda fresh.
// (We could trust rec.body_glsl, but re-running ensures forward-compat
// when we tweak the translator.)
auto tr = fn::gfx::code_to_generator(rec.source_glsl);
if (!tr.ok) continue; // skip broken records
fn::gfx::DagNodeDef def = fn::gfx::make_generator_def(rec.id, rec.label, rec.description, tr);
fn::gfx::dag_register_node(def);
}
}
static void render() {
// Apply pending layout BEFORE any ImGui::Begin this frame.
// (LoadIniSettingsFromMemory must happen before windows are submitted.)
std::string applied = fn_ui::layout_storage_apply_pending(g_layouts);
if (!applied.empty()) g_layout_cb.active_name = applied;
if (!g_canvas_code.initialized) fn::gfx::canvas_init(g_canvas_code);
if (!g_canvas_dag.initialized) fn::gfx::canvas_init(g_canvas_dag);
if (g_code_dirty) {
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - g_code_last_edit).count();
if (elapsed > 250) {
compile_code();
g_code_dirty = false;
}
}
if (g_dag_dirty) {
compile_dag();
g_dag_dirty = false;
}
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
// Menubar (View + Layouts + Settings + About) la invoca fn::run_app a
// partir de AppConfig::panels y AppConfig::layouts_cb.
// --- Code window ---
if (g_show_code) {
if (ImGui::Begin("Code", &g_show_code)) {
if (fn_ui::button("Save as generator...", fn_ui::ButtonVariant::Secondary)) {
g_save_modal_open = true;
g_save_err.clear();
}
if (fn_ui::modal_dialog_begin("Save as generator", &g_save_modal_open,
ImVec2(420, 0))) {
ImGui::TextUnformatted("Guardar shader actual como nodo Gen del DAG.");
ImGui::Spacing();
fn_ui::text_input("name (snake_case)", g_save_name, sizeof(g_save_name),
"ej: my_shader");
fn_ui::text_input("label", g_save_label, sizeof(g_save_label));
ImGui::InputTextMultiline("description", g_save_desc, sizeof(g_save_desc),
ImVec2(380, 60));
fn_ui::text_input("tags (CSV)", g_save_tags, sizeof(g_save_tags));
if (!g_save_err.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error);
ImGui::TextWrapped("%s", g_save_err.c_str());
ImGui::PopStyleColor();
}
ImGui::Separator();
if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) {
g_save_modal_open = false;
}
ImGui::SameLine();
if (fn_ui::button("Save", fn_ui::ButtonVariant::Primary)) {
g_save_err = save_current_as_generator();
if (g_save_err.empty()) {
g_save_modal_open = false;
}
}
}
fn_ui::modal_dialog_end();
ImVec2 avail = ImGui::GetContentRegionAvail();
float footer_h = g_code_err.empty() ? 0.0f : ImGui::GetTextLineHeightWithSpacing() + 8.0f;
ImVec2 editor_size(avail.x, avail.y - footer_h);
char buf[1 << 16];
size_t copy_len = g_source.size() < sizeof(buf) - 1 ? g_source.size() : sizeof(buf) - 1;
memcpy(buf, g_source.c_str(), copy_len);
buf[copy_len] = '\0';
if (ImGui::InputTextMultiline("##code", buf, sizeof(buf), editor_size,
ImGuiInputTextFlags_AllowTabInput)) {
g_source = buf;
mark_code_dirty();
}
draw_err(g_code_err, g_code_err_line);
}
ImGui::End();
}
// --- DAG Pipeline window ---
if (g_show_dag) {
if (ImGui::Begin("DAG Pipeline", &g_show_dag)) {
if (fn::gfx::dag_node_editor(g_pipeline)) {
g_dag_dirty = true;
}
draw_err(g_dag_err, g_dag_err_line);
}
ImGui::End();
}
// --- Canvas Code ---
// Code is fully independent from the DAG: only the uniforms declared in
// the Code source itself (parsed via parse_uniforms) get fed. To reproduce
// a DAG render here, paste the *baked* "Generated GLSL" — its u_params live
// as a const array inside the source.
if (g_show_canvas_c) {
if (ImGui::Begin("Canvas Code", &g_show_canvas_c)) {
fn::gfx::canvas_render(g_canvas_code, static_cast<float>(ImGui::GetTime()),
[](unsigned int program) {
fn::gfx::uniforms_apply(g_store, g_descs, program);
});
}
ImGui::End();
}
// --- Canvas DAG ---
if (g_show_canvas_d) {
if (ImGui::Begin("Canvas DAG", &g_show_canvas_d)) {
fn::gfx::canvas_render(g_canvas_dag, static_cast<float>(ImGui::GetTime()),
[](unsigned int program) {
fn::gfx::dag_uniforms_apply(g_pipeline, program);
});
}
ImGui::End();
}
if (g_canvas_dag.program) {
fn::gfx::dag_previews_render(g_pipeline, g_canvas_dag.program);
}
// --- Controls window (Code uniforms) ---
if (g_show_controls) {
if (ImGui::Begin("Controls", &g_show_controls)) {
if (g_descs.empty()) {
ImGui::TextDisabled("No uniforms declared in Code.");
ImGui::TextDisabled("Use // @slider, @color, @toggle, @xy annotations.");
} else {
fn::gfx::uniforms_panel(g_store, g_descs);
}
// fps_overlay ahora se renderiza desde fn::run_app cuando el usuario
// lo activa en Settings → Show FPS overlay.
}
ImGui::End();
}
// --- Functions palette (drag into DAG Pipeline) ---
if (g_show_functions) {
if (ImGui::Begin("Functions", &g_show_functions)) {
fn::gfx::dag_palette();
}
ImGui::End();
}
// --- Generated GLSL window (self-contained DAG → paste-able into Code) ---
// We bake the live params into a `const vec4 u_params[]` so the displayed
// text is a complete shader: copy-pasting it into the Code editor yields
// the same render at the moment of the copy, and nothing in the DAG can
// change the Code canvas afterwards.
if (g_show_generated) {
if (ImGui::Begin("Generated GLSL", &g_show_generated)) {
if (g_pipeline.empty()) {
ImGui::TextDisabled("(DAG not compiled yet)");
} else {
static std::string s_baked;
s_baked = fn::gfx::compile_dag_to_glsl_baked(g_pipeline);
ImVec2 avail = ImGui::GetContentRegionAvail();
ImGui::InputTextMultiline("##dag_glsl",
const_cast<char*>(s_baked.c_str()),
s_baked.size() + 1,
avail,
ImGuiInputTextFlags_ReadOnly);
}
}
ImGui::End();
}
}
int main() {
fn::gfx::shaderlab_db_open("shaders_lab.db");
load_user_generators_into_catalog();
ensure_dag_default();
// Layout persistence: handle opaco que crea su propia tabla
// imgui_layouts en shaders_lab.db (CREATE IF NOT EXISTS, no toca la
// tabla ui_layouts heredada). Cualquier app del registry puede usar
// este patron.
g_layouts = fn_ui::layout_storage_open("shaders_lab.db");
fn_ui::layout_storage_make_callbacks(g_layouts, g_layout_cb);
// Override de on_reset: ademas de limpiar el INI, re-mostrar todos
// los paneles especificos de shaders_lab.
g_layout_cb.on_reset = []() {
g_show_code = g_show_dag = g_show_canvas_c = g_show_canvas_d =
g_show_controls = g_show_functions = g_show_generated = true;
ImGui::LoadIniSettingsFromMemory("", 0);
ImGui::GetIO().WantSaveIniSettings = true;
g_layout_cb.active_name.clear();
};
fn::AppConfig cfg;
cfg.title = "shaders_lab";
cfg.width = 1600;
cfg.height = 900;
cfg.about = {.name = "shaders_lab",
.version = "0.3.0",
.description = "Live GLSL shader playground with DAG pipeline. layout_storage publico, compiler extraido, AppConfig estandar, multi-viewport, modal save-as via modal_dialog."};
cfg.panels = k_panels;
cfg.panel_count = sizeof(k_panels) / sizeof(k_panels[0]);
cfg.layouts_cb = &g_layout_cb;
cfg.log = {"shaders_lab.log", 1};
cfg.auto_dockspace = false; // shaders_lab gestiona su propio DockSpace en render()
int rc = fn::run_app(cfg, render);
fn::gfx::canvas_destroy(g_canvas_code);
fn::gfx::canvas_destroy(g_canvas_dag);
fn::gfx::dag_node_editor_destroy();
fn::gfx::dag_previews_destroy();
fn_ui::layout_storage_close(g_layouts);
fn::gfx::shaderlab_db_close();
return rc;
}
-15
View File
@@ -1,15 +0,0 @@
# Smoke test app para validar que text_editor + file_watcher compilan
# y enlazan correctamente. NO es una app del registry, solo build gate
# de las funciones nuevas del issue 0025. Sin ImGui events runtime el
# test crea, settea texto, polea y destruye en 1 frame headless (no abre ventana).
add_imgui_app(text_editor_smoke
main.cpp
${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp
${CMAKE_SOURCE_DIR}/functions/core/file_poll_diff.cpp
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit/TextEditor.cpp
)
target_include_directories(text_editor_smoke PRIVATE
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit
)
-28
View File
@@ -1,28 +0,0 @@
---
name: text_editor_smoke
lang: cpp
domain: tools
description: "Smoke test CLI (sin GUI) que valida los wrappers PIMPL de text_editor y file_watcher (inotify Linux / ReadDirectoryChangesW Win). No abre ventana ImGui — solo crea/settea texto/lee/poll/destruye."
tags: [cpp, smoke, test, cli]
uses_functions:
- text_editor_cpp_core
- file_watcher_cpp_core
uses_types: []
framework: "cli"
entry_point: "main.cpp"
dir_path: "cpp/apps/text_editor_smoke"
repo_url: ""
---
# text_editor_smoke
Smoke test que verifica las APIs de `text_editor` y `file_watcher` linkean correctamente. Sin ventana ImGui.
## Build & run
```bash
cd cpp && cmake --build build --target text_editor_smoke -j
./build/text_editor_smoke
```
Salida esperada: log con bytes leidos del editor + eventos del file_watcher.
-64
View File
@@ -1,64 +0,0 @@
// Smoke test (no GUI): compila y ejecuta brevemente las APIs nuevas del
// issue 0025 para validar que el wrapper PIMPL del text_editor y el
// file_watcher (inotify Linux / ReadDirectoryChangesW Win) enlazan.
//
// No abre ventana ImGui — solo crea / settea texto / lee / poll / destruye.
#include "core/text_editor.h"
#include "core/file_watcher.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <thread>
#include <chrono>
int main() {
// ----- text_editor -----
auto* ed = fn::text_editor_create(fn::CodeLang::GLSL);
if (!ed) { std::fprintf(stderr, "text_editor_create returned null\n"); return 1; }
fn::text_editor_set_text(ed, "void main(){}\n");
const char* got = fn::text_editor_get_text(ed);
std::printf("text_editor: get_text -> %zu bytes\n", got ? std::strlen(got) : 0u);
if (fn::text_editor_is_dirty(ed)) {
std::fprintf(stderr, "text_editor: dirty unexpected after set_text\n");
return 1;
}
fn::text_editor_destroy(ed);
// ----- file_watcher -----
const char* path = "/tmp/fn_smoke_test.txt";
std::remove(path);
{
FILE* f = std::fopen(path, "w"); std::fputs("init\n", f); std::fclose(f);
}
auto* fw = fn::file_watcher_create();
if (!fw) { std::fprintf(stderr, "file_watcher_create returned null\n"); return 1; }
if (!fn::file_watcher_add(fw, path)) {
std::fprintf(stderr, "file_watcher_add failed: %s\n", fn::file_watcher_last_error(fw));
// Aun asi continuamos: en CI sin inotify (raro) este test seria flaky.
}
// Modificar
{
FILE* f = std::fopen(path, "w"); std::fputs("changed\n", f); std::fclose(f);
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
auto evs = fn::file_watcher_poll(fw);
std::printf("file_watcher: %zu events\n", evs.size());
for (auto& e : evs) {
const char* kind = e.kind == fn::FileEvent::Modified ? "MOD"
: e.kind == fn::FileEvent::Created ? "NEW" : "DEL";
std::printf(" [%s] %s\n", kind, e.path.c_str());
}
fn::file_watcher_destroy(fw);
std::remove(path);
std::printf("OK\n");
return 0;
}
+221 -17
View File
@@ -23,12 +23,14 @@
#include <filesystem> #include <filesystem>
#include <string> #include <string>
#include <sys/stat.h> #include <sys/stat.h>
#include <unordered_map>
#ifdef _WIN32 #ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN #ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#endif #endif
#include <windows.h> #include <windows.h>
#include <windowsx.h> // GET_X_LPARAM / GET_Y_LPARAM
#define GLFW_EXPOSE_NATIVE_WIN32 #define GLFW_EXPOSE_NATIVE_WIN32
#include <GLFW/glfw3native.h> #include <GLFW/glfw3native.h>
#else #else
@@ -57,37 +59,148 @@ static void glfw_error_callback(int error, const char* description) {
// the existing buffer pixels. We replicate that contract: while sizemove is // the existing buffer pixels. We replicate that contract: while sizemove is
// active, skip render + glfwSwapBuffers, only pump the message queue. As soon // active, skip render + glfwSwapBuffers, only pump the message queue. As soon
// as WM_EXITSIZEMOVE arrives, normal rendering resumes. // as WM_EXITSIZEMOVE arrives, normal rendering resumes.
//
// IMPORTANT: the subclass must cover EVERY HWND owned by the process — main
// window AND every secondary viewport platform window the ImGui GLFW backend
// creates when the user drags a panel outside the main. Otherwise AltSnap on
// a secondary HWND would not be observed, the main loop would keep rendering,
// and the visible jitter would return on that panel. g_in_sizemove stays
// global on purpose: any external move on ANY of our HWNDs pauses the whole
// render pipeline, exactly like the native title-bar drag contract.
static std::atomic<bool> g_in_sizemove{false}; static std::atomic<bool> g_in_sizemove{false};
static WNDPROC g_orig_wndproc = nullptr; // Test observability — monotonic counters. fn::internal exposes accessors.
static HWND g_subclassed_hwnd = nullptr; static std::atomic<int> g_sizemove_enter_count{0};
static std::atomic<int> g_alt_rmb_resize_count{0};
static std::atomic<int> g_alt_lmb_move_count{0};
// Test hook — bypasses GetAsyncKeyState(VK_MENU) so headless tests can drive
// the Alt+RMB / Alt+LMB paths without UI-access for keybd_event.
static std::atomic<bool> g_force_alt_for_test{false};
// Diagnostic: every WM_RBUTTONDOWN this subclass sees (Alt-or-not). Used to
// distinguish "message never arrived" from "Alt check failed".
static std::atomic<int> g_rbuttondown_seen_count{0};
// Accessed only from the main (render) thread. Map value is the original
// WNDPROC for that HWND so we can restore and chain CallWindowProcW.
static std::unordered_map<HWND, WNDPROC> g_subclassed;
// Pick the WMSZ_* direction whose modal resize will feel natural depending
// on which quadrant of the client rect the cursor is in. Matches AltSnap's
// quadrant rule (top-left -> shrink toward top-left, etc.).
static int alt_rmb_resize_direction(HWND hwnd, int client_x, int client_y) {
RECT rc{};
if (!GetClientRect(hwnd, &rc)) return 8 /* WMSZ_BOTTOMRIGHT */;
int cx = (rc.right - rc.left) / 2;
int cy = (rc.bottom - rc.top) / 2;
bool top = (client_y < cy);
bool left = (client_x < cx);
if (top && left) return 4; // WMSZ_TOPLEFT
if (top && !left) return 5; // WMSZ_TOPRIGHT
if (!top && left) return 7; // WMSZ_BOTTOMLEFT
return 8; // WMSZ_BOTTOMRIGHT
}
static LRESULT CALLBACK fn_subclass_wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { static LRESULT CALLBACK fn_subclass_wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) { switch (msg) {
case WM_ENTERSIZEMOVE: case WM_ENTERSIZEMOVE:
g_in_sizemove.store(true, std::memory_order_release); g_in_sizemove.store(true, std::memory_order_release);
g_sizemove_enter_count.fetch_add(1, std::memory_order_acq_rel);
break; break;
case WM_EXITSIZEMOVE: case WM_EXITSIZEMOVE:
g_in_sizemove.store(false, std::memory_order_release); g_in_sizemove.store(false, std::memory_order_release);
break; break;
case WM_LBUTTONDOWN:
// Alt + LMB anywhere on the window initiates a native modal MOVE
// via WM_SYSCOMMAND, SC_MOVE | HTCAPTION. Same pattern as our
// Alt+RMB resize: ReleaseCapture, post the syscommand, return 0
// to consume the click. Windows then drives a normal move modal
// (DefWindowProc blocks the thread) and our existing
// WM_ENTERSIZEMOVE gate pauses render so there's no jitter.
{
bool alt_real = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0;
bool alt_test = g_force_alt_for_test.load(std::memory_order_acquire);
if (alt_real || alt_test) {
g_alt_lmb_move_count.fetch_add(1, std::memory_order_acq_rel);
if (alt_test) {
// Test mode: skip SC_MOVE post to keep the harness
// out of Windows' modal move loop.
return 0;
}
ReleaseCapture();
PostMessageW(hwnd, WM_SYSCOMMAND,
(WPARAM)(0xF010 /* SC_MOVE */ | 2 /* HTCAPTION */), 0);
return 0;
}
}
break;
case WM_RBUTTONDOWN:
g_rbuttondown_seen_count.fetch_add(1, std::memory_order_acq_rel);
// Alt + RMB anywhere on the window initiates a native modal
// resize. Direction is chosen by cursor quadrant relative to
// window center so dragging "feels" like grabbing the nearest
// corner. ReleaseCapture is required before WM_SYSCOMMAND
// SC_SIZE so the modal loop can take input. The subsequent
// WM_ENTERSIZEMOVE bracket is observed above, so render is
// gated for free and no jitter appears.
{
bool alt_real = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0;
bool alt_test = g_force_alt_for_test.load(std::memory_order_acquire);
if (alt_real || alt_test) {
int cx = GET_X_LPARAM(lp);
int cy = GET_Y_LPARAM(lp);
int dir = alt_rmb_resize_direction(hwnd, cx, cy);
g_alt_rmb_resize_count.fetch_add(1, std::memory_order_acq_rel);
if (alt_test) {
// Test mode: skip the SC_SIZE post to keep the modal
// out of the headless test harness. The counter is
// sufficient to verify the path was taken; the modal
// entry is exercised in the real-input manual test.
return 0;
}
ReleaseCapture();
PostMessageW(hwnd, WM_SYSCOMMAND,
(WPARAM)(0xF000 /* SC_SIZE */ | dir), 0);
return 0; // consume so ImGui doesn't see a right-click
}
}
break;
default: break; default: break;
} }
return CallWindowProcW(g_orig_wndproc, hwnd, msg, wp, lp); auto it = g_subclassed.find(hwnd);
WNDPROC orig = (it != g_subclassed.end()) ? it->second : nullptr;
if (orig) return CallWindowProcW(orig, hwnd, msg, wp, lp);
return DefWindowProcW(hwnd, msg, wp, lp);
}
static void install_sizemove_subclass_hwnd(HWND hwnd) {
if (!hwnd) return;
if (g_subclassed.find(hwnd) != g_subclassed.end()) return; // idempotent
WNDPROC orig = (WNDPROC)SetWindowLongPtrW(
hwnd, GWLP_WNDPROC, (LONG_PTR)fn_subclass_wndproc);
g_subclassed[hwnd] = orig;
} }
static void install_sizemove_subclass(GLFWwindow* w) { static void install_sizemove_subclass(GLFWwindow* w) {
HWND hwnd = glfwGetWin32Window(w); if (!w) return;
if (!hwnd) return; install_sizemove_subclass_hwnd(glfwGetWin32Window(w));
g_subclassed_hwnd = hwnd;
g_orig_wndproc = (WNDPROC)SetWindowLongPtrW(
hwnd, GWLP_WNDPROC, (LONG_PTR)fn_subclass_wndproc);
} }
static void uninstall_sizemove_subclass() { // Reap stale entries: when a secondary viewport is destroyed (panel re-docked
if (g_subclassed_hwnd && g_orig_wndproc) { // back into main), the GLFW backend calls glfwDestroyWindow and the HWND is
SetWindowLongPtrW(g_subclassed_hwnd, GWLP_WNDPROC, (LONG_PTR)g_orig_wndproc); // invalidated. Drop those entries so we don't hold dangling pointers and so a
// fresh HWND at the same address gets re-subclassed cleanly.
static void prune_dead_subclassed() {
for (auto it = g_subclassed.begin(); it != g_subclassed.end();) {
if (!IsWindow(it->first)) it = g_subclassed.erase(it);
else ++it;
} }
g_subclassed_hwnd = nullptr; }
g_orig_wndproc = nullptr;
static void uninstall_sizemove_subclass_all() {
for (auto& kv : g_subclassed) {
if (IsWindow(kv.first) && kv.second) {
SetWindowLongPtrW(kv.first, GWLP_WNDPROC, (LONG_PTR)kv.second);
}
}
g_subclassed.clear();
} }
static inline bool external_sizemove_active() { static inline bool external_sizemove_active() {
@@ -312,6 +425,15 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
// Title-bar-only move for ImGui windows. Critical for secondary viewports
// (floating panels) whose entire OS window is a single borderless ImGui
// window: without this flag, ImGui moves the window when the user drags
// any empty client-area pixel, which translates to the OS viewport
// following the mouse "from anywhere" with no modifier. With this flag,
// floating panels obey the same "header only" contract as a native
// decorated window. Alt+LMB anywhere still moves via our WndProc subclass
// (consumed before ImGui sees the click).
io.ConfigWindowsMoveFromTitleBarOnly = true;
// Convencion local_files: imgui.ini y app_settings.ini viven en // Convencion local_files: imgui.ini y app_settings.ini viven en
// <exe_dir>/local_files/. Migra automaticamente desde el cwd o // <exe_dir>/local_files/. Migra automaticamente desde el cwd o
@@ -406,17 +528,59 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
while (!glfwWindowShouldClose(window)) { while (!glfwWindowShouldClose(window)) {
glfwPollEvents(); glfwPollEvents();
// When the main window is iconified we used to glfwWaitEvents+continue
// to save CPU. That is wrong when floating panels (secondary
// viewports) exist: skipping the frame stops UpdatePlatformWindows /
// RenderPlatformWindowsDefault so those panels go blank or get
// ungrouped by the WM. We therefore detect secondary viewports first
// and, if any are alive, fall through to a normal frame (main GL
// clear/swap is harmless on the hidden main HWND, secondary GL
// contexts keep refreshing). Only when there are NO floating panels
// do we sleep on glfwWaitEvents the way we used to.
if (glfwGetWindowAttrib(window, GLFW_ICONIFIED)) { if (glfwGetWindowAttrib(window, GLFW_ICONIFIED)) {
glfwWaitEvents(); bool has_secondary_viewport = false;
continue; if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
ImGuiPlatformIO& pio_ic = ImGui::GetPlatformIO();
for (int i = 0; i < pio_ic.Viewports.Size; ++i) {
ImGuiViewport* vp = pio_ic.Viewports[i];
if (!vp || !vp->PlatformHandle) continue;
if ((GLFWwindow*)vp->PlatformHandle == window) continue;
has_secondary_viewport = true;
break;
}
}
if (!has_secondary_viewport) {
glfwWaitEvents();
continue;
}
// fallthrough: render normally so floating panels stay alive.
} }
#ifdef _WIN32
// Subclass any platform window we haven't subclassed yet. Covers the
// main window AND every secondary viewport (panels dragged outside
// main) so AltSnap's WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE brackets are
// observed regardless of which HWND it targets. Runs BEFORE the
// sizemove gate below so newly-created secondaries are protected from
// their very first frame onwards.
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
prune_dead_subclassed();
ImGuiPlatformIO& pio_sub = ImGui::GetPlatformIO();
for (int i = 0; i < pio_sub.Viewports.Size; ++i) {
ImGuiViewport* vp = pio_sub.Viewports[i];
if (!vp || !vp->PlatformHandle) continue;
install_sizemove_subclass((GLFWwindow*)vp->PlatformHandle);
}
}
#endif
// While an external mover (AltSnap on Win32, tiling WMs) is dragging // While an external mover (AltSnap on Win32, tiling WMs) is dragging
// the window we mirror the native title-bar contract: do not render, // the window we mirror the native title-bar contract: do not render,
// do not swap, just pump events. The DWM compositor scrolls the last // do not swap, just pump events. The DWM compositor scrolls the last
// presented framebuffer with the window — no race between SetWindowPos // presented framebuffer with the window — no race between SetWindowPos
// (async) and glfwSwapBuffers, so no jitter. WM_EXITSIZEMOVE clears // (async) and glfwSwapBuffers, so no jitter. WM_EXITSIZEMOVE clears
// the flag and the main loop resumes normal rendering. // the flag and the main loop resumes normal rendering. Applies to
// brackets on ANY subclassed HWND (main or secondary viewports).
if (external_sizemove_active()) { if (external_sizemove_active()) {
// Bound the busy loop so the message queue gets drained but we // Bound the busy loop so the message queue gets drained but we
// don't burn CPU when AltSnap pauses between mouse moves. // don't burn CPU when AltSnap pauses between mouse moves.
@@ -576,7 +740,7 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
ImPlot::DestroyContext(); ImPlot::DestroyContext();
ImGui::DestroyContext(); ImGui::DestroyContext();
#ifdef _WIN32 #ifdef _WIN32
uninstall_sizemove_subclass(); uninstall_sizemove_subclass_all();
#endif #endif
glfwDestroyWindow(window); glfwDestroyWindow(window);
glfwTerminate(); glfwTerminate();
@@ -588,6 +752,46 @@ int run_app(std::function<void()> render_fn) {
return run_app(AppConfig{}, render_fn); return run_app(AppConfig{}, render_fn);
} }
// Test-only observability of the Win32 subclass. Always defined (zero cost);
// on non-Windows the counters never increment.
namespace internal {
int sizemove_enter_count() {
#ifdef _WIN32
return g_sizemove_enter_count.load(std::memory_order_acquire);
#else
return 0;
#endif
}
int alt_rmb_resize_count() {
#ifdef _WIN32
return g_alt_rmb_resize_count.load(std::memory_order_acquire);
#else
return 0;
#endif
}
void set_force_alt_for_test(bool v) {
#ifdef _WIN32
g_force_alt_for_test.store(v, std::memory_order_release);
#else
(void)v;
#endif
}
int rbuttondown_seen_count() {
#ifdef _WIN32
return g_rbuttondown_seen_count.load(std::memory_order_acquire);
#else
return 0;
#endif
}
int alt_lmb_move_count() {
#ifdef _WIN32
return g_alt_lmb_move_count.load(std::memory_order_acquire);
#else
return 0;
#endif
}
} // namespace internal
} // namespace fn } // namespace fn
#ifdef IMGUI_ENABLE_TEST_ENGINE #ifdef IMGUI_ENABLE_TEST_ENGINE
+11
View File
@@ -176,6 +176,17 @@ int run_app(AppConfig config, std::function<void()> render_fn);
// Convenience: run with default config // Convenience: run with default config
int run_app(std::function<void()> render_fn); int run_app(std::function<void()> render_fn);
// Test-only observability hooks for the Win32 anti-jitter / Alt+RMB resize
// subclass. Counters increment monotonically across the life of the process.
// On non-Windows targets they always return 0.
namespace internal {
int sizemove_enter_count();
int alt_rmb_resize_count();
int alt_lmb_move_count();
int rbuttondown_seen_count();
void set_force_alt_for_test(bool v);
}
} // namespace fn } // namespace fn
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
+75
View File
@@ -0,0 +1,75 @@
// auto_detect_type — infiere ColumnType muestreando celdas de una columna.
// Promovido del playground tables (issue 0081-F). Pura — sin I/O ni estado.
#include "core/auto_detect_type.h"
#include <cstdlib>
#include <cstring>
namespace data_table {
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;
}
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;
}
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 puro: no se acumula en 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;
}
} // namespace data_table
+30
View File
@@ -0,0 +1,30 @@
// auto_detect_type — infiere ColumnType muestreando celdas de una columna.
// Promovido del playground tables (issue 0081-F). Pura — sin I/O ni estado.
#pragma once
#include "core/data_table_types.h"
namespace data_table {
// Devuelve true si la cadena s es un literal booleano ("true" / "false").
bool is_bool_text(const char* s);
// Devuelve true si s tiene formato de fecha ISO YYYY-MM-DD (minimo 10 chars).
bool is_date_iso(const char* s);
// Devuelve true si s empieza por '{' o '[' (ignora espacios iniciales).
bool is_json_text(const char* s);
// Devuelve true si s es un entero decimal con signo opcional.
bool is_integer_text(const char* s);
// Parsea s como double. Devuelve false si s esta vacio o no es numerico.
bool parse_number(const char* s, double& out);
// auto_detect_type: escanea hasta sample_n celdas no-vacias de la columna col
// en la matriz cells (row-major, rows x cols) y devuelve el ColumnType inferido.
// Retorna ColumnType::String si no hay celdas validas o si hay mezcla.
ColumnType auto_detect_type(const char* const* cells, int rows, int cols,
int col, int sample_n = 64);
} // namespace data_table
+69
View File
@@ -0,0 +1,69 @@
---
name: auto_detect_type
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "ColumnType auto_detect_type(const char* const* cells, int rows, int cols, int col, int sample_n = 64)"
description: "Infiere el ColumnType de una columna escaneando hasta sample_n celdas no-vacias. Detecta Int, Float, Bool, Date (ISO YYYY-MM-DD), Json ({/[) y String."
tags: [tables, stats, inference, type-detection, tql, cpp-tables]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["core/auto_detect_type.h"]
tested: true
tests:
- "auto_detect_type: columna numerica entera"
- "auto_detect_type: columna de texto libre"
- "auto_detect_type: columna de fechas ISO"
- "auto_detect_type: columna booleana"
- "auto_detect_type: columna float"
- "auto_detect_type: mezcla numerica y texto retorna String"
- "auto_detect_type: columna vacia retorna String"
- "auto_detect_type: sample_n=2 evalua solo las primeras 2 celdas no-vacias"
test_file_path: "cpp/tests/test_auto_detect_type.cpp"
file_path: "cpp/functions/core/auto_detect_type.cpp"
params:
- name: cells
desc: "Matriz row-major de punteros a C-strings (rows x cols). Puede contener nullptr para celdas nulas."
- name: rows
desc: "Numero de filas de la matriz."
- name: cols
desc: "Numero de columnas de la matriz."
- name: col
desc: "Indice de la columna a analizar (0-based)."
- name: sample_n
desc: "Maximo de celdas no-vacias a escanear. Default 64. Usar valor menor para columnas muy largas."
output: "ColumnType inferido: Int si todos son enteros, Float si hay decimales, Bool si 'true'/'false', Date si YYYY-MM-DD, Json si '{' o '[', String en cualquier otro caso o mezcla."
---
## Ejemplo
```cpp
#include "core/auto_detect_type.h"
// Columna de fechas: ["2024-01-01", "2024-06-15", "2023-12-31"]
const char* dates[] = {"2024-01-01", "2024-06-15", "2023-12-31"};
data_table::ColumnType t = data_table::auto_detect_type(dates, 3, 1, 0);
// t == ColumnType::Date
// Columna de enteros
const char* nums[] = {"1", "42", "100"};
t = data_table::auto_detect_type(nums, 3, 1, 0);
// t == ColumnType::Int
```
## Cuando usarla
Cuando recibas datos tabulares sin tipo declarado y necesites inferirlo antes de aplicar filtros numericos, formateo de celdas o estadisticas. Tipicamente se llama una vez al cargar o refrescar la fuente de datos.
## Gotchas
- Requiere que ALL las celdas no-vacias sean del mismo tipo para retornar ese tipo. Una sola celda texto en una columna de enteros devuelve `ColumnType::String`.
- Bool se evalua antes que Date y Number, por eso "true"/"false" no se confunde con texto.
- La deteccion de Date solo reconoce ISO 8601 (`YYYY-MM-DD`). Formatos locales (DD/MM/YYYY) caen en String.
- `sample_n` no considera el orden aleatorio: evalua las primeras N celdas no-vacias del principio. Si los datos estan ordenados por tipo, ajustar `sample_n`.
@@ -0,0 +1,94 @@
// compute_column_stats — estadisticas por columna (mean, p25/p50/p75, hist, top).
// Promovido del playground tables (issue 0081-F). Pura — sin I/O ni estado.
#include "core/compute_column_stats.h"
#include <algorithm>
#include <cmath>
#include <unordered_map>
#include <vector>
namespace data_table {
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<std::string, int> counts;
if (unique_cap > 0) counts.reserve(std::min(unique_cap, n));
bool all_numeric = true;
std::vector<double> 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 descendente.
if (!counts.empty()) {
std::vector<std::pair<std::string,int>> v(counts.begin(), counts.end());
int topN = std::min<int>(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 val : nums) {
int b = (int)((val - 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;
}
} // namespace data_table
+43
View File
@@ -0,0 +1,43 @@
// compute_column_stats — estadisticas por columna (mean, p25/p50/p75, hist, top).
// Promovido del playground tables (issue 0081-F). Pura — sin I/O ni estado.
#pragma once
#include "core/auto_detect_type.h" // parse_number reutilizado en la impl
#include <string>
#include <utility>
#include <vector>
namespace data_table {
// ColStats: estadisticas calculadas para una columna de la tabla.
// Tipo producto (todos los campos siempre presentes).
struct ColStats {
int total = 0; // filas consideradas (respeta indices si se pasa)
int empty_count = 0; // celdas nulas o vacias
int unique_count = 0; // valores distintos (hasta unique_cap)
bool unique_capped = false; // true si se llego al limite unique_cap
bool numeric = false; // true si TODOS los no-vacios son numericos
int numeric_count = 0;
double min = 0;
double max = 0;
double sum = 0;
double mean = 0;
double p25 = 0;
double p50 = 0;
double p75 = 0;
std::vector<float> hist; // HIST_BINS bins normalizados por count
std::vector<std::pair<std::string,int>> top_categories; // top-8 por frecuencia desc
};
constexpr int HIST_BINS = 24;
// compute_column_stats: calcula ColStats para la columna col de cells (row-major,
// rows x cols). Si indices != nullptr, solo considera las filas listadas en
// indices[0..n_indices-1] (util para filtrar). unique_cap limita el tracking de
// valores unicos para columnas de cardinalidad alta.
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
@@ -0,0 +1,75 @@
---
name: compute_column_stats
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "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)"
description: "Calcula estadisticas completas para una columna: mean, p25/p50/p75, min/max, count de vacios, unicos, histograma de 24 bins y top-8 categorias por frecuencia."
tags: [tables, stats, statistics, histogram, percentile, tql, cpp-tables]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["core/compute_column_stats.h"]
tested: true
tests:
- "compute_column_stats: media correcta sobre vector numerico"
- "compute_column_stats: p50 es mediana"
- "compute_column_stats: p25 y p75 correctos"
- "compute_column_stats: conteo de vacios correcto"
- "compute_column_stats: columna texto no es numerica"
- "compute_column_stats: unique_count correcto"
- "compute_column_stats: top_categories ordena por frecuencia desc"
- "compute_column_stats: indices filtra filas correctamente"
- "compute_column_stats: histograma generado para numerica"
- "compute_column_stats: columna vacia retorna stats en cero"
- "compute_column_stats: col fuera de rango devuelve ColStats defecto"
test_file_path: "cpp/tests/test_compute_column_stats.cpp"
file_path: "cpp/functions/core/compute_column_stats.cpp"
params:
- name: cells
desc: "Matriz row-major de punteros a C-strings (rows x cols). nullptr indica celda nula."
- name: rows
desc: "Numero de filas de la matriz."
- name: cols
desc: "Numero de columnas de la matriz."
- name: col
desc: "Indice de la columna a analizar (0-based)."
- name: unique_cap
desc: "Maximo de valores unicos a trackear. Default 100000. Con 0 trackea todos."
- name: indices
desc: "Array opcional de indices de fila a incluir. Si nullptr se usan todas las filas."
- name: n_indices
desc: "Longitud del array indices. Ignorado si indices es nullptr."
output: "ColStats con total, empty_count, unique_count, numeric, numeric_count, min, max, sum, mean, p25, p50, p75, hist (HIST_BINS=24 floats) y top_categories (hasta 8 pares string+count)."
---
## Ejemplo
```cpp
#include "core/compute_column_stats.h"
// Columna: ["10", "20", "30", "40", "50"]
const char* data[] = {"10", "20", "30", "40", "50"};
data_table::ColStats s = data_table::compute_column_stats(data, 5, 1, 0);
// s.mean == 30.0, s.p50 == 30.0, s.p25 == 20.0, s.p75 == 40.0
// Con filtro de filas (solo filas 0, 2, 4 -> [10, 30, 50]):
int idx[] = {0, 2, 4};
s = data_table::compute_column_stats(data, 5, 1, 0, 100000, idx, 3);
// s.mean == 30.0, s.total == 3
```
## Cuando usarla
Cuando necesites mostrar un panel de estadisticas de columna (inspector, tooltip de header, sidebar de datos), calcular min/max para escalar un histograma, o detectar columnas con mucho missing antes de aplicar un filtro.
## Gotchas
- `numeric` es `true` solo si TODOS los valores no-vacios son numericos. Una sola celda texto hace que `numeric == false` y `hist` quede vacio.
- El histograma usa interpolacion lineal para los percentiles (igual que numpy `percentile` con `method='linear'`). Para distribuciones con un solo valor, el bin central recibe todo el count.
- `unique_capped` se activa cuando hay mas valores distintos que `unique_cap`. En ese caso `unique_count == unique_cap` y puede no reflejar la cardinalidad real.
- Las `top_categories` incluyen valores numericos si la columna no es puramente numerica (mezcla). Si la columna es puramente numerica, `top_categories` lista los valores numericos como strings.
+39
View File
@@ -0,0 +1,39 @@
// compute_pipeline — Pure TQL pipeline execution.
// Chains compute_stage calls: output of stage N becomes input of stage N+1.
// Promoted from cpp/apps/primitives_gallery/playground/tables/ (issue 0081-B).
#include "core/compute_pipeline.h"
#include <vector>
#include <string>
namespace data_table {
StageOutput compute_pipeline(const char* const* in_cells, int in_rows, int in_cols,
const std::vector<std::string>& in_headers,
const std::vector<ColumnType>& in_types,
const std::vector<Stage>& stages)
{
if (stages.empty()) {
// Passthrough: wrap raw input.
Stage empty_stage;
return compute_stage(in_cells, in_rows, in_cols, in_headers, in_types, empty_stage);
}
// Run first stage against raw input.
StageOutput prev = compute_stage(in_cells, in_rows, in_cols, in_headers, in_types, stages[0]);
// Chain remaining stages: each consumes the previous StageOutput.
for (size_t i = 1; i < stages.size(); ++i) {
StageOutput next = compute_stage(
prev.cells.empty() ? nullptr : prev.cells.data(),
prev.rows,
prev.cols,
prev.headers,
prev.types,
stages[i]);
prev = std::move(next);
}
return prev;
}
} // namespace data_table
+21
View File
@@ -0,0 +1,21 @@
// compute_pipeline — Pure TQL pipeline: chains N stages to produce a final StageOutput.
// Part of issue 0081-B: promoted from primitives_gallery playground to registry.
// Depends on compute_stage.h. No ImGui, no I/O.
#pragma once
#include "core/compute_stage.h"
#include "core/data_table_types.h"
#include <string>
#include <vector>
namespace data_table {
// compute_pipeline — Pure: applies stages[0..N-1] in sequence.
// Each stage receives the StageOutput of the previous one as input.
// If stages is empty, returns a passthrough StageOutput wrapping in_cells.
StageOutput compute_pipeline(const char* const* in_cells, int in_rows, int in_cols,
const std::vector<std::string>& in_headers,
const std::vector<ColumnType>& in_types,
const std::vector<Stage>& stages);
} // namespace data_table
+81
View File
@@ -0,0 +1,81 @@
---
name: compute_pipeline
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "data_table::StageOutput data_table::compute_pipeline(const char* const* in_cells, int in_rows, int in_cols, const std::vector<std::string>& in_headers, const std::vector<ColumnType>& in_types, const std::vector<Stage>& stages)"
description: "Chains N TQL stages sequentially: output of stage i becomes input of stage i+1. Returns the final StageOutput. Empty stages returns a passthrough. No ImGui, no I/O."
tags: [tables, pipeline, tql, chain, pure, cpp-tables]
uses_functions: [compute_stage_cpp_core]
uses_types: [data_table_types_cpp_core]
returns: []
returns_optional: false
error_type: ""
imports: []
tested: true
tests:
- "compute_pipeline empty stages returns passthrough"
- "compute_pipeline single stage equals compute_stage"
- "compute_pipeline two stages chain filter then group"
- "compute_pipeline three stage chain"
- "compute_pipeline empty table"
test_file_path: "cpp/tests/test_compute_pipeline.cpp"
file_path: "cpp/functions/core/compute_pipeline.cpp"
params:
- name: in_cells
desc: "Row-major cell array (in_rows * in_cols pointers). May be null when in_rows == 0."
- name: in_rows
desc: "Number of input rows."
- name: in_cols
desc: "Number of columns per row. Must match in_headers.size()."
- name: in_headers
desc: "Column names for the raw input. Passed as-is to stage 0."
- name: in_types
desc: "Declared ColumnType per column for the raw input."
- name: stages
desc: "Ordered list of Stage descriptors. Each stage runs on the output of the previous one. Empty = passthrough."
output: "StageOutput produced by the last stage. Owns its cell_backing. If stages is empty, returns a no-op passthrough of in_cells."
---
## Ejemplo
```cpp
#include "core/compute_pipeline.h"
using namespace data_table;
const char* raw[] = {
"EU","A","100", "US","A","200",
"EU","B","300", "EU","A","50"
};
std::vector<std::string> hdrs = {"region","type","revenue"};
std::vector<ColumnType> types = {ColumnType::String, ColumnType::String, ColumnType::Float};
// Stage 0: keep EU rows only
Stage s0;
Filter f; f.col=0; f.op=Op::Eq; f.value="EU"; s0.filters.push_back(f);
// Stage 1: group by type, sum revenue, sort desc
Stage s1;
s1.breakouts = {"type"};
Aggregation agg; agg.fn=AggFn::Sum; agg.col="revenue"; s1.aggregations.push_back(agg);
SortClause sc; sc.col="sum_revenue"; sc.desc=true; s1.sorts.push_back(sc);
StageOutput out = compute_pipeline(raw, 4, 3, hdrs, types, {s0, s1});
// out.rows == 2 (B=300, A=150), sorted desc
// out.cells[0]=="B", out.cells[1]=="300"
// out.cells[2]=="A", out.cells[3]=="150"
```
## Cuando usarla
- Cuando tienes una secuencia de transformaciones TQL (filter -> group -> filter -> sort) y quieres ejecutarlas todas en un solo call.
- Para implementar el "active pipeline" de un State con multiples stages: `compute_pipeline(raw, rows, cols, hdrs, types, state.stages)`.
- En tests de integracion de pipelines multi-stage sin renderizar nada.
## Gotchas
- Cada stage crea un `StageOutput` intermedio con su propio `cell_backing`. Los intermedios se destruyen al final de la llamada — solo el resultado final sobrevive.
- Los punteros de `out.cells` del ultimo stage pueden apuntar a backing de ese stage o a cells del input del stage anterior (passthrough rows). El caller solo necesita mantener vivo el `StageOutput` devuelto — no el `in_cells` original si hubo algun stage que agrego rows al backing.
- Si `stages` es vacio, se comporta igual que `compute_stage` con un `Stage{}` vacio (passthrough, copia punteros de `in_cells` sin copiar datos).
+525
View File
@@ -0,0 +1,525 @@
// compute_stage — Pure TQL stage execution.
// Promoted from cpp/apps/primitives_gallery/playground/tables/data_table_logic.cpp
// (issue 0081-B). No ImGui, no I/O. Helper statics are file-private.
#include "core/compute_stage.h"
#include "core/tql_helpers.h" // aggregation_alias, agg_fn_token (avoids ODR dup)
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace data_table {
ColumnType aggregation_type(const Aggregation& a,
const std::vector<std::string>& in_headers,
const std::vector<ColumnType>& 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;
}
std::vector<int> apply_filters(const char* const* cells, int rows, int cols,
const std::vector<Filter>& filters)
{
std::vector<int> 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];
// compare inline: numeric if both parseable, lexical otherwise
auto do_compare = [](const char* a, const char* b, Op op) -> bool {
if (!a) a = "";
if (!b) b = "";
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;
char* ea = nullptr; char* eb = nullptr;
na = std::strtod(a, &ea); nb = std::strtod(b, &eb);
bool numeric = (ea != a && *ea == '\0') && (eb != b && *eb == '\0');
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;
};
if (!do_compare(cell, f.value.c_str(), f.op)) { keep = false; break; }
}
if (keep) out.push_back(r);
}
return out;
}
// ----------------------------------------------------------------------------
// File-private helpers (static)
// ----------------------------------------------------------------------------
static bool parse_num(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;
}
namespace {
int find_col(const std::vector<std::string>& headers, const std::string& name) {
for (size_t i = 0; i < headers.size(); ++i) if (headers[i] == name) return (int)i;
return -1;
}
int cmp_cells(const char* a, const char* b) {
if (!a) a = ""; if (!b) b = "";
double na, nb;
bool num = parse_num(a, na) && parse_num(b, nb);
if (num) return (na < nb) ? -1 : (na > nb ? 1 : 0);
return std::strcmp(a, b);
}
void apply_sorts(std::vector<int>& row_idx,
const char* const* cells, int cols,
const std::vector<std::string>& headers,
const std::vector<SortClause>& sorts)
{
if (sorts.empty()) return;
std::vector<int> 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<double>& 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<double>& 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;
}
// parse_breakout_granularity: strip optional ":granularity" suffix.
// DateGranularity is defined in data_table_types.h (issue 0081).
DateGranularity parse_breakout_granularity_local(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 = DateGranularity::None;
if (suffix == "year") g = DateGranularity::Year;
else if (suffix == "month") g = DateGranularity::Month;
else if (suffix == "week") g = DateGranularity::Week;
else if (suffix == "day") g = DateGranularity::Day;
else if (suffix == "hour") g = DateGranularity::Hour;
if (g == DateGranularity::None) {
col_out = breakout;
return DateGranularity::None;
}
col_out = breakout.substr(0, pos);
return g;
}
// truncate_date: truncate ISO date string to given granularity.
std::string truncate_date_local(const std::string& date, DateGranularity g) {
if (g == DateGranularity::None) return date;
if (date.size() < 10) return date;
// Parse YYYY-MM-DD
auto d = [](char c){ return c >= '0' && c <= '9'; };
if (!d(date[0])||!d(date[1])||!d(date[2])||!d(date[3])) return date;
if (date[4] != '-') return date;
if (!d(date[5])||!d(date[6])) return date;
if (date[7] != '-') return date;
if (!d(date[8])||!d(date[9])) return date;
int y = (date[0]-'0')*1000+(date[1]-'0')*100+(date[2]-'0')*10+(date[3]-'0');
int m = (date[5]-'0')*10+(date[6]-'0');
int dd_= (date[8]-'0')*10+(date[9]-'0');
if (m < 1 || m > 12 || dd_ < 1 || dd_ > 31) 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, dd_);
return buf;
case DateGranularity::Hour: {
int hh = 0;
if (date.size() >= 13 && date[10] == 'T'
&& d(date[11]) && d(date[12])) {
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, dd_, hh);
return buf;
}
case DateGranularity::Week: {
// Hinnant civil calendar: ymd -> days since 0000-03-01.
// days%7: 0=Wed. Monday offset: ((days%7 - 5 + 7) % 7).
auto ymd_to_days = [](int yy, int mm, int dd2) -> long {
if (mm <= 2) { yy -= 1; mm += 12; }
long era = (yy >= 0 ? yy : yy - 399) / 400;
unsigned yoe = (unsigned)(yy - era * 400);
unsigned doy = (unsigned)((153*(mm-3)+2)/5 + dd2 - 1);
unsigned doe = yoe*365 + yoe/4 - yoe/100 + doy;
return era * 146097 + (long)doe;
};
auto days_to_ymd = [](long days, int& yy, int& mm, int& dd2) {
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 day2 = doy - (153*mp+2)/5 + 1;
unsigned mon = mp < 10 ? mp+3 : mp-9;
if (mon <= 2) yr += 1;
yy = yr; mm = (int)mon; dd2 = (int)day2;
};
long days = ymd_to_days(y, m, dd_);
int mod = (int)(((days % 7) + 7) % 7);
int rem = ((mod - 5) % 7 + 7) % 7;
long monday = days - rem;
int yy2, mm2, dd2;
days_to_ymd(monday, yy2, mm2, dd2);
std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d", yy2, mm2, dd2);
return buf;
}
default: return date;
}
}
} // anon namespace
// ----------------------------------------------------------------------------
// compute_stage (public)
// ----------------------------------------------------------------------------
StageOutput compute_stage(const char* const* in_cells, int in_rows, int in_cols,
const std::vector<std::string>& in_headers,
const std::vector<ColumnType>& in_types,
const Stage& stage)
{
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: same shape, filtered + sorted.
out.cols = in_cols;
out.headers = in_headers;
out.types = in_types;
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: group visible rows by breakout values, compute aggregations.
int nbreaks = (int)stage.breakouts.size();
std::vector<int> break_cols(nbreaks);
std::vector<DateGranularity> break_grans(nbreaks);
bool any_trunc = false;
for (int i = 0; i < nbreaks; ++i) {
std::string col_name;
break_grans[i] = parse_breakout_granularity_local(stage.breakouts[i], col_name);
if (break_grans[i] != DateGranularity::None) any_trunc = true;
break_cols[i] = find_col(in_headers, col_name);
}
// Pre-allocate cell_backing to avoid pointer invalidation on push_back.
out.cell_backing.reserve(
(size_t)in_rows * (size_t)nbreaks +
(size_t)in_rows * stage.aggregations.size() + 16);
std::vector<const char*> 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_local(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';
k += cell_for(r, i);
}
return k;
};
std::unordered_map<std::string, int> key_to_group;
std::vector<std::string> group_keys;
std::vector<std::vector<int>> group_rows;
std::vector<std::vector<const char*>> group_breakvals;
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<const char*> 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);
}
// Build output headers + types: 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];
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 per group.
int n_groups = (int)group_rows.size();
// Reserve exact size to prevent pointer invalidation.
out.cell_backing.reserve(out.cell_backing.size() + (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();
};
std::vector<const char*> flat;
flat.reserve((size_t)n_groups * out_cols);
for (int gi = 0; gi < n_groups; ++gi) {
for (size_t i = 0; i < stage.breakouts.size(); ++i) {
flat.push_back(group_breakvals[gi][i]);
}
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<std::string> 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 over strings: preserve type
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<double> 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_num(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 groups by stage.sorts (col-name lookup in out.headers).
std::vector<int> 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;
}
// ---------------------------------------------------------------------------
// State method implementations (declared in data_table_types.h).
// Placed here as the header documents "defined in compute_stage.cpp".
// Promoted from playground data_table_logic.cpp — issue 0081-I Wave 3.5.
// ---------------------------------------------------------------------------
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];
}
} // namespace data_table
+39
View File
@@ -0,0 +1,39 @@
// compute_stage — Pure TQL stage execution (filter → group+agg|passthrough → sort).
// Part of issue 0081-B: promoted from primitives_gallery playground to registry.
// No ImGui, no I/O. Depends only on data_table_types.h.
#pragma once
#include "core/data_table_types.h"
#include <string>
#include <vector>
namespace data_table {
// ----------------------------------------------------------------------------
// aggregation_alias — Pure: default alias when agg.alias is empty.
// count -> "count"
// distinct col -> "distinct_<col>"
// percentile p -> "p<arg*100>_<col>" (e.g. p95_size_kb)
// other -> "<fn>_<col>" (e.g. avg_size_kb)
// ----------------------------------------------------------------------------
std::string aggregation_alias(const Aggregation& a);
// aggregation_type — Pure: output ColumnType of an aggregation.
ColumnType aggregation_type(const Aggregation& a,
const std::vector<std::string>& in_headers,
const std::vector<ColumnType>& in_types);
// apply_filters — Pure: returns indices of rows passing all filters.
// Uses compare() for each cell vs filter value/op.
std::vector<int> apply_filters(const char* const* cells, int rows, int cols,
const std::vector<Filter>& filters);
// compute_stage — Pure: executes one Stage over in_cells.
// Pipeline: apply_filters -> (group+agg | passthrough) -> sort.
// Returns a StageOutput owning its cell_backing and cells pointer list.
StageOutput compute_stage(const char* const* in_cells, int in_rows, int in_cols,
const std::vector<std::string>& in_headers,
const std::vector<ColumnType>& in_types,
const Stage& stage);
} // namespace data_table
+80
View File
@@ -0,0 +1,80 @@
---
name: compute_stage
kind: function
lang: cpp
domain: core
version: "1.1.0"
purity: pure
signature: "data_table::StageOutput data_table::compute_stage(const char* const* in_cells, int in_rows, int in_cols, const std::vector<std::string>& in_headers, const std::vector<ColumnType>& in_types, const Stage& stage)"
description: "Executes one TQL Stage over a cell matrix: apply_filters -> (group+agg | passthrough) -> sort. Returns a StageOutput owning its cell backing. No ImGui, no I/O."
tags: [tables, pipeline, tql, filter, aggregation, sort, pure, cpp-tables]
uses_functions: [tql_helpers_cpp_core]
uses_types: [data_table_types_cpp_core]
returns: []
returns_optional: false
error_type: ""
imports:
- "core/tql_helpers.h"
tested: true
tests:
- "compute_stage passthrough returns all rows"
- "compute_stage filter eq keeps matching rows"
- "compute_stage group sum aggregates correctly"
- "compute_stage sort desc reorders rows"
- "compute_stage filter then group then sort asc"
- "apply_filters returns correct row indices"
- "aggregation_alias produces expected names"
- "compute_stage empty table empty stage"
test_file_path: "cpp/tests/test_compute_stage.cpp"
file_path: "cpp/functions/core/compute_stage.cpp"
params:
- name: in_cells
desc: "Row-major cell array (in_rows * in_cols pointers). May be null when in_rows == 0."
- name: in_rows
desc: "Number of input rows. 0 returns empty StageOutput."
- name: in_cols
desc: "Number of columns per row. Must match in_headers.size()."
- name: in_headers
desc: "Column names for in_cells. Used for breakout/sort/agg column lookup by name."
- name: in_types
desc: "Declared ColumnType per column. Used for aggregation output type and date truncation."
- name: stage
desc: "Stage descriptor: filters (row predicates), breakouts+aggregations (group-by), sorts (output order)."
output: "StageOutput with rows/cols/headers/types and cells pointer list into cell_backing. cell_backing owns all newly allocated strings (aggregation results, truncated dates). cells pointing to in_cells are not copied."
---
## Ejemplo
```cpp
#include "core/compute_stage.h"
using namespace data_table;
// 3 rows x 2 cols
const char* raw[] = {"eng","90000", "mktg","70000", "eng","95000"};
std::vector<std::string> hdrs = {"dept","salary"};
std::vector<ColumnType> types = {ColumnType::String, ColumnType::Int};
// Stage: filter dept=eng, group by dept, sum salary, sort desc
Stage s;
Filter f; f.col=0; f.op=Op::Eq; f.value="eng"; s.filters.push_back(f);
s.breakouts = {"dept"};
Aggregation agg; agg.fn=AggFn::Sum; agg.col="salary"; s.aggregations.push_back(agg);
SortClause sc; sc.col="sum_salary"; sc.desc=true; s.sorts.push_back(sc);
StageOutput out = compute_stage(raw, 3, 2, hdrs, types, s);
// out.rows == 1, out.headers == {"dept","sum_salary"}
// out.cells[0] == "eng", out.cells[1] == "185000"
```
## Cuando usarla
- Cuando necesites aplicar filtros, agrupaciones y ordenacion sobre datos tabulares raw (cell array) en un solo paso.
- Como bloque de construccion de `compute_pipeline` para procesar stage por stage.
- En tests headless de logica TQL sin renderizar nada.
## Gotchas
- `cell_backing` crece con `emplace_back`; la reserva previa evita reallocs durante grupos. No guardes punteros a `cell_backing` antes de llamar a la funcion — usa `out.cells` del resultado.
- Las celdas de passthrough apuntan directamente a `in_cells` (sin copiar). El caller debe mantener `in_cells` vivo mientras use `StageOutput`.
- Breakouts con sufijo `:granularity` (`:year`, `:month`, `:week`, `:day`, `:hour`) truncan fechas ISO. Breakouts sin sufijo usan el valor raw.
- `apply_filters` y `aggregation_alias` son funciones publicas del mismo header — usables independientemente.
+19 -1
View File
@@ -71,6 +71,24 @@ void load_fonts_from_settings() {
const AppSettings& s = settings(); const AppSettings& s = settings();
const float size_px = s.font_size_px; const float size_px = s.font_size_px;
// Extended glyph range for the text font. ImGui's default is Latin-1 only,
// which leaves bullets/dots/arrows/checkmarks as `?`. Adds:
// General Punctuation (em-dash, ellipsis, smart quotes)
// Geometric Shapes (● ▲ ■ ▼ ▶ ◀ — used by data_table Dots renderer)
// Miscellaneous Symbols (★ ☆ ☑ ☒ ♦)
// Dingbats (✓ ✔ ✗ ✘ ✱ ❤)
// Misc Symbols & Arrows (⬆ ⬇ ⬅ ➡)
// Geometric Shapes Ext (🭨 etc.) — kept narrow to avoid bloat
static const ImWchar text_ranges[] = {
0x0020, 0x00FF, // Basic Latin + Latin-1
0x2010, 0x2027, // General Punctuation (em-dash, ellipsis, quotes)
0x25A0, 0x25FF, // Geometric Shapes (● ▲ ■ ◆ ...)
0x2600, 0x26FF, // Misc Symbols (★ ♥ ☑)
0x2700, 0x27BF, // Dingbats (✓ ✗ ✱)
0x2B00, 0x2BFF, // Arrows + Misc Symbols (⬆ ⬇ ⬅ ➡)
0,
};
// 1. Texto. // 1. Texto.
g_text_loaded = false; g_text_loaded = false;
if (s.font_id == FontId::ProggyClean) { if (s.font_id == FontId::ProggyClean) {
@@ -84,7 +102,7 @@ void load_fonts_from_settings() {
cfg.OversampleH = 2; cfg.OversampleH = 2;
cfg.OversampleV = 1; cfg.OversampleV = 1;
cfg.PixelSnapH = false; cfg.PixelSnapH = false;
if (io.Fonts->AddFontFromFileTTF(ttf.c_str(), size_px, &cfg)) { if (io.Fonts->AddFontFromFileTTF(ttf.c_str(), size_px, &cfg, text_ranges)) {
g_text_loaded = true; g_text_loaded = true;
} else { } else {
std::fprintf(stderr, "[fn_ui] AddFontFromFileTTF fallo (%s)\n", ttf.c_str()); std::fprintf(stderr, "[fn_ui] AddFontFromFileTTF fallo (%s)\n", ttf.c_str());
+174
View File
@@ -0,0 +1,174 @@
// join_tables — Pure multi-key hash join between two tables.
// Promoted from cpp/apps/primitives_gallery/playground/tables/ (issue 0081-E).
// No ImGui, no I/O.
#include "core/join_tables.h"
#include <string>
#include <unordered_map>
#include <vector>
namespace data_table {
namespace {
// Find column index by name in a header list. Returns -1 if not found.
static int find_col_idx(const std::vector<std::string>& hdrs, const std::string& name) {
for (size_t i = 0; i < hdrs.size(); ++i)
if (hdrs[i] == name) return (int)i;
return -1;
}
// Build a composite key string from multiple columns in a row.
// Each key component is separated by \x1f (unit separator, unlikely in data).
// Missing or null columns produce an empty component followed by |\x1f to
// distinguish "missing column" from "empty value in a present column".
static std::string make_key(const char* const* cells, int row, int cols,
const std::vector<int>& 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";
}
return k;
}
} // anon
StageOutput join_tables(const char* const* left_cells, int left_rows, int left_cols,
const std::vector<std::string>& left_headers,
const std::vector<ColumnType>& left_types,
const TableInput& right,
const Join& jn)
{
StageOutput out;
// Resolve key column indices in left and right.
std::vector<int> 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));
}
// Determine which right columns to include in output.
std::vector<int> 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: all left columns, then right fields (aliased).
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 name = (rc < (int)right.headers.size()) ? right.headers[rc] : "";
std::string prefixed = jn.alias.empty() ? name : (jn.alias + "." + name);
out.headers.push_back(std::move(prefixed));
out.types.push_back(rc < (int)right.types.size() ? right.types[rc] : ColumnType::Auto);
}
// Hash right rows by composite key.
std::unordered_map<std::string, std::vector<int>> 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);
}
// Track which right rows matched (needed for right/full outer).
std::vector<bool> right_matched(right.rows, false);
// Pre-size backing storage to avoid reallocation invalidating pointers.
out.cell_backing.reserve((size_t)(left_rows + right.rows) * (size_t)out.cols);
// Helpers to append rows into cell_backing.
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 >= 0 && rr < right.rows && rc >= 0 && rc < right.cols)
? right.cells[rr * right.cols + rc] : nullptr;
out.cell_backing.emplace_back(s ? s : "");
}
};
auto append_right_empty = [&]() {
for (int rc : right_fields) { (void)rc; out.cell_backing.emplace_back(""); }
};
// Strategy flags.
bool keep_unmatched_left = (jn.strategy == JoinStrategy::Left || jn.strategy == JoinStrategy::Full);
bool keep_unmatched_right = (jn.strategy == JoinStrategy::Right || jn.strategy == JoinStrategy::Full);
// For Right join we still iterate left to find matches; unmatched left rows are dropped.
bool iterate_left = (jn.strategy != JoinStrategy::Right)
? true
: true; // always iterate left to mark right_matched
int row_count = 0;
if (iterate_left) {
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()) {
// No match on right side.
if (keep_unmatched_left) {
append_left_row(lr);
append_right_empty();
++row_count;
}
// For Right/Inner: skip this left row.
continue;
}
// Matched: produce one output row per matching right row (Cartesian for duplicates).
for (int rr : it->second) {
// For Right join: emit only if we keep left+right matches.
// Inner/Left/Full all emit matched rows.
if (jn.strategy == JoinStrategy::Right) {
// Right join: left side appears for matched rows, left cells come first.
append_left_row(lr);
} else {
append_left_row(lr);
}
append_right_row(rr);
right_matched[rr] = true;
++row_count;
}
}
}
// Append unmatched right rows (Right join / Full outer).
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;
// Build pointer list after all backing strings are stable.
out.cells.reserve(out.cell_backing.size());
for (auto& s : out.cell_backing) out.cells.push_back(s.c_str());
return out;
}
} // namespace data_table
+38
View File
@@ -0,0 +1,38 @@
// join_tables — Pure multi-key hash join between two tables.
// Promoted from cpp/apps/primitives_gallery/playground/tables/ (issue 0081-E).
// No ImGui, no I/O. Depends only on data_table_types.h.
#pragma once
#include "core/data_table_types.h"
#include <string>
#include <vector>
namespace data_table {
// join_tables — Pure: applies one Join spec over a left table and a TableInput right.
//
// Strategy:
// Inner — only rows with at least one matching key in both sides.
// Left — all left rows; unmatched left rows get empty right cells.
// Right — all right rows; unmatched right rows get empty left cells.
// Full — union of left and right: unmatched rows from both sides included.
//
// Multi-key: jn.on is a list of {left_col, right_col} pairs; all pairs must
// match for a row to be considered a hit (AND semantics). Missing key columns
// (name not found in headers) produce an empty string for that key component.
//
// Key duplicates: produces Cartesian product for matching rows. Each (left_i,
// right_j) pair where both keys match becomes a separate output row.
//
// Output columns: all left columns, then the right columns listed in jn.fields
// (or all right columns if jn.fields is empty). Right column names are prefixed
// with "jn.alias." when jn.alias is non-empty.
//
// Returns a StageOutput owning its cell_backing and cells pointer list.
StageOutput join_tables(const char* const* left_cells, int left_rows, int left_cols,
const std::vector<std::string>& left_headers,
const std::vector<ColumnType>& left_types,
const TableInput& right,
const Join& jn);
} // namespace data_table

Some files were not shown because too many files have changed in this diff Show More