fix(infra): gradle_run detecta android-sdk — issue 0076 #2
+198
-6
@@ -18,7 +18,13 @@ uses_functions:
|
||||
- dag_palette_cpp_gfx
|
||||
- dag_node_editor_cpp_gfx
|
||||
- dag_node_previews_cpp_gfx
|
||||
- shaderlab_db_cpp_gfx
|
||||
- code_to_generator_cpp_gfx
|
||||
- fps_overlay_cpp_core
|
||||
- panel_menu_cpp_core
|
||||
- layouts_menu_cpp_core
|
||||
- app_menubar_cpp_core
|
||||
- layout_storage_sqlite_cpp_core
|
||||
uses_types: []
|
||||
framework: "imgui + opengl3 + imgui-node-editor"
|
||||
entry_point: "cpp/build/linux/apps/shaders_lab/shaders_lab"
|
||||
@@ -88,15 +94,164 @@ App de live-coding y composicion de fragment shaders GLSL con dos modos coexiste
|
||||
- `dag_previews_render` itera nodos con preview abierto, dibuja al FBO con ese index.
|
||||
- Sin recompile al togglear preview ni al mover sliders — un solo programa GL.
|
||||
|
||||
### Fase 5 — SQLite + custom generators desde el Code `[done]`
|
||||
- **`u_params` a tamaño dinámico**: array global `vec4 u_params[64]` (256 floats), cada nodo ocupa `ceil(param_count/4)` vec4s consecutivos. `dag_param_layout(pipeline)` calcula el indice base por nodo; compilador y `dag_uniforms_apply` lo comparten. `DagStep::params` y `DagNodeDef::param_*` pasan a `vector<>`.
|
||||
- **Nuevos Gen nodes (8)**: `checker`, `stripes`, `dots`, `rings`, `polar_rays`, `noise_value`, `voronoi`, `truchet`. Catalogo total: 19 nodos (4 originales + 8 nuevos Gen + 4 Op + 3 Blend + Output).
|
||||
- **Bug fix `solid`**: el control Color con `ImGuiColorEditFlags_NoLabel` no mostraba el nombre. Ahora `dag_node_editor` imprime `TextUnformatted(label) + SameLine` antes del swatch.
|
||||
- **Persistencia `shaders_lab.db`** (SQLite local en `apps/shaders_lab/shaders_lab.db`): tabla `generators` con `id, label, description, source_glsl, body_glsl, param_count, param_defaults, param_names, controls, tags, timestamps`. Funcion `shaderlab_db` (CRUD) testeada (7/7) y reutilizable.
|
||||
- **Catalogo mutable**: `dag_register_node()` / `dag_unregister_node()`. Built-ins protegidos via flag `is_builtin`.
|
||||
- **Code → Generator**: funcion pura `code_to_generator(source)` traduce el GLSL del Code en un body de Gen + DagControl[] (testeada 7/7). Cada uniform anotado se convierte en su control (slider/xy/color); cada uniform reclama 1 vec4 entero. El body se transforma asi: lineas `vec2 uv = ...` eliminadas, `fragColor = X;` -> `return X;`, locales `<type> <name> = u_params[__BASE__+i].swizzle;` prependidas. La lambda `body_glsl` substituye `__BASE__` con el indice runtime.
|
||||
- **UI**: boton `Save as generator...` en el panel `Code` con modal (name snake_case + label + description + tags). Tras guardar, el nodo aparece en la paleta `Functions`. Al arrancar, `load_user_generators_into_catalog()` re-traduce y registra los persistidos.
|
||||
- **Quitados**: botones de presets `Plasma / Circle / Checker` y el archivo `seed_shaders.h`. Default del Code = un placeholder con uniforms anotados como ejemplo.
|
||||
|
||||
### Fase 6 — Menubar reusable (View + Layouts) `[done]`
|
||||
|
||||
App estrena una `BeginMainMenuBar` con dos menus, cableada via `app_menubar_cpp_core`:
|
||||
|
||||
- **View** (`panel_menu_cpp_core`): MenuItem checkable por cada uno de los 7 paneles (`Code`, `DAG Pipeline`, `Canvas Code`, `Canvas DAG`, `Controls`, `Functions`, `Generated GLSL`). Cada bool `g_show_*` se comparte con el `bool*` de `ImGui::Begin(name, &g_show_X)`, asi que la X de cada ventana sincroniza con el menu. Cada `Begin/End` envuelto en guard para no llamar `End` si el panel esta oculto.
|
||||
|
||||
- **Layouts** (`layouts_menu_cpp_core`): captura del layout actual de ImGui (`SaveIniSettingsToMemory`) bajo un nombre, persistido en la tabla `ui_layouts(name, blob, created_at, updated_at)` de `shaders_lab.db`. Items:
|
||||
- Lista de layouts guardados (click → apply, marker `* ` en el activo).
|
||||
- `Save current as...` (popup con InputText).
|
||||
- `Delete` (submenu listando los layouts).
|
||||
- `Reset to default` (abre todos los paneles, limpia marker activo).
|
||||
|
||||
Detalles tecnicos:
|
||||
- `LoadIniSettingsFromMemory` se difiere al inicio del frame siguiente via `g_pending_layout_blob` (no se puede llamar mid-frame entre `NewFrame` y `Render`).
|
||||
- `shaders_lab.db` se reutiliza para `ui_layouts` via nuevo getter `shaderlab_db_handle()` — una sola conexion SQLite para generators y layouts.
|
||||
- Las callbacks (`list/on_apply/on_save/on_delete/on_reset`) se cablean en `main()` con lambdas que envuelven las primitivas CRUD de `layout_storage_sqlite_cpp_core`.
|
||||
|
||||
### Como usarlo en otras apps
|
||||
|
||||
Patron reusable de tres pasos:
|
||||
|
||||
```cpp
|
||||
#include "core/app_menubar.h"
|
||||
#include "core/layout_storage_sqlite.h"
|
||||
|
||||
// 1. Declarar bools de visibilidad por panel
|
||||
static bool g_show_foo = true;
|
||||
static bool g_show_bar = true;
|
||||
|
||||
// 2. Declarar callbacks y blob diferido
|
||||
static fn_ui::LayoutCallbacks g_layout_cb;
|
||||
static std::string g_pending_blob;
|
||||
static std::string g_pending_name;
|
||||
|
||||
// 3. En main(), cablear callbacks contra tu sqlite3*
|
||||
fn_ui::layout_storage_init(db);
|
||||
g_layout_cb.list = [db]{ return fn_ui::layout_storage_list(db); };
|
||||
g_layout_cb.on_apply = [db](const std::string& n) {
|
||||
g_pending_blob = fn_ui::layout_storage_load_blob(db, n);
|
||||
g_pending_name = n;
|
||||
};
|
||||
g_layout_cb.on_save = [db](const std::string& n) {
|
||||
size_t sz = 0;
|
||||
const char* b = ImGui::SaveIniSettingsToMemory(&sz);
|
||||
if (b && sz) fn_ui::layout_storage_save(db, n, std::string(b, sz));
|
||||
g_layout_cb.active_name = n;
|
||||
};
|
||||
g_layout_cb.on_delete = [db](const std::string& n) {
|
||||
fn_ui::layout_storage_delete(db, n);
|
||||
if (g_layout_cb.active_name == n) g_layout_cb.active_name.clear();
|
||||
};
|
||||
g_layout_cb.on_reset = []{ /* abrir todos los paneles, limpiar active_name */ };
|
||||
|
||||
// 4. En render(), aplicar pendientes y llamar app_menubar
|
||||
void render() {
|
||||
if (!g_pending_blob.empty()) {
|
||||
ImGui::LoadIniSettingsFromMemory(g_pending_blob.c_str(), g_pending_blob.size());
|
||||
g_layout_cb.active_name = g_pending_name;
|
||||
g_pending_blob.clear(); g_pending_name.clear();
|
||||
}
|
||||
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
|
||||
|
||||
fn_ui::PanelToggle toggles[] = {
|
||||
{"Foo", "Ctrl+1", &g_show_foo},
|
||||
{"Bar", "Ctrl+2", &g_show_bar},
|
||||
};
|
||||
fn_ui::app_menubar(toggles, std::size(toggles), &g_layout_cb);
|
||||
|
||||
if (g_show_foo) {
|
||||
if (ImGui::Begin("Foo", &g_show_foo)) { /* ... */ }
|
||||
ImGui::End();
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Fase 7 — UX node editor + DAG correctness `[done]` (2026-04-25)
|
||||
|
||||
Pulido de la edición visual del DAG y corrección de fugas en el render. Sin cambio de schema ni de catalog público (más allá del `dag_register_node` ya añadido en Fase 5).
|
||||
|
||||
- **Nodos más grandes para conectar más rápido** (`dag_node_editor.cpp`):
|
||||
- `PIN_RADIUS` 9 → **14 px** (área de grab ~2.5×). `PIN_DIAMETER`, `CABLE_THICK` 2.5 → **3.5**, borde de pin 1.5 → 2.0.
|
||||
- `CONTROL_WIDTH` constante 150 → **220 px**, `COL_GAP` 8 → **14 px**, `NodePadding` vertical 8 → 12.
|
||||
- Espaciado inicial entre nodos auto-colocados 220 → 320 px.
|
||||
- **Bug fix `solid` sin label**: el control `Color` usaba `ImGuiColorEditFlags_NoLabel`, así que el swatch era el único contenido del nodo y parecía "sin nombre ni parámetro". Fix en `dag_node_editor.cpp`: imprimir `ImGui::TextUnformatted(ctrl.label) + SameLine` antes del swatch. Aplica a todo control de tipo `Color`, no solo a `solid`.
|
||||
- **Strict output** (`dag_compile.cpp`): eliminado el fallback `last_valid_out` que filtraba el output del último nodo evaluado cuando `Output` no tenía source o no existía. Ahora la regla es: solo se emite lo conectado al nodo `Output`; en cualquier otro caso `seed()` (gris oscuro `vec4(0.04, 0.04, 0.06, 1.0)`). El `resolve()` de inputs internos también dejó de caer a `last_valid_out` y ahora emite `vec4(0,0,0,1)` para slots sin conectar. Tests: `dag_compile` 6/6 → **7/7** (test 4b verifica que el seed final aparece después de las branches de preview, no antes).
|
||||
- **Generated GLSL autocontenido** (`compile_dag_to_glsl_baked`, nuevo en `dag_compile.{h,cpp}`):
|
||||
- Sustituye `uniform vec4 u_params[64]` por `const vec4 u_params[N] = vec4[N](vec4(...), ...)` con los valores actuales del pipeline empaquetados (mismo layout que `dag_uniforms_apply`).
|
||||
- Sustituye `uniform int u_preview_target` por `const int u_preview_target = -1` (las branches de preview quedan muertas y el GLSL compiler las elimina).
|
||||
- Resultado: el shader del panel `Generated GLSL` no depende de ningún uniform externo. Pegarlo en el editor `Code` reproduce exactamente el render del DAG en el momento del copy. Después editar el DAG no afecta al Code.
|
||||
- Test 7 nuevo: `dag_compile_baked` no contiene `uniform vec4 u_params` ni `uniform int u_preview_target`, sí contiene `const vec4 u_params[` y los valores empaquetados.
|
||||
- **Importante**: el `Canvas Code` ya NO recibe `dag_uniforms_apply`. Es totalmente independiente. (Versión anterior intentaba sincronizarlos; rompía el aislamiento entre paneles.)
|
||||
- **`dag_uniforms_apply` también resetea `u_preview_target = -1`** al final, para que la rama de preview quede desactivada en el render principal del Canvas DAG. La rutina `dag_previews_render` la activa de forma transitoria por nodo y la deja restaurada.
|
||||
- **Drop-replace del mismo kind**:
|
||||
- Soltar un nodo de la paleta sobre un nodo existente del **mismo `DagKind`** (Gen sobre Gen, Op sobre Op, Blend sobre Blend, nunca sobre Output) sustituye `name`+`params`+`controls` conservando `id`, `editor_uid`, `editor_pos_x/y`, `source_ids[]` y `preview_open`.
|
||||
- Slots de input que sobran (si el nuevo def tiene menos `num_inputs` que el anterior) se limpian.
|
||||
- Hit-test contra cajas de nodos vía `ed::GetNodePosition` + `ed::GetNodeSize` (canvas-space). No se usa `ed::GetHoveredNode()` porque no es fiable durante un drag-drop activo.
|
||||
- **Drop-on-cable splice (intercalar nodo)**:
|
||||
- Soltar un nodo de la paleta **o** arrastrar un nodo Op/Blend ya existente sobre un cable: el nodo se inserta entre `src` y `dst`. `new.source_ids[0] = src.id`, `dst.source_ids[slot] = new.id`. Para Blend (2 inputs), slot 0 queda cableado y slot 1 vacío.
|
||||
- Para nodos existentes movidos: además de las dos rewires anteriores, se limpian todas las refs hacia el nodo movido en otros `source_ids[]` antes (lo desengancha de cualquier consumidor previo, queda exclusivamente en la nueva posición). Tracking del nodo arrastrado vía `s_drag_existing_uid` (set en `IsMouseClicked(0)` cuando hay un nodo hovered y no hay pin hovered, def es Gen/Op/Blend, no Output).
|
||||
- Hit-test del cable: distancia punto-segmento (`dist_point_to_segment`) entre el cursor y la línea aproximada `(src.right_mid → dst.left_at_slot_k)`. Threshold **18 px** canvas-space.
|
||||
- Prioridad: cable-hit > node-hit > add-vacío.
|
||||
- **Splice highlight (preview visual)**:
|
||||
- Mientras hay un drag activo de paleta o de nodo del canvas, el cable candidato se redibuja en `SPLICE_COLOR = (1.00, 0.82, 0.18, 1)` (dorado) más grueso (`CABLE_THICK + 2`).
|
||||
- **Garantía visual**: además de cambiar el color en `ed::Link()`, se dibuja un bezier dorado encima en el `ImGui::GetForegroundDrawList()` (canvas → screen via `ed::CanvasToScreen`). Esto evita problemas de compositing interno del editor que podían enterrar el cambio de color.
|
||||
- Detección sin gates: la versión anterior gateaba con `IsMouseDown` + `window_hovered`, lo que silenciaba el highlight. Ahora basta con la presencia del payload de drag-drop (paleta) o del `s_drag_existing_uid` (nodo del canvas).
|
||||
- **Catalog `dag_catalog.cpp`** ya soporta `is_builtin` (Fase 5) y permite `dag_register_node` / `dag_unregister_node` para generators custom; el splice/replace funciona sobre todos por igual (Built-ins, Gen custom guardados desde Code).
|
||||
|
||||
Comandos:
|
||||
```bash
|
||||
# Build linux
|
||||
./fn run build_cpp_linux_bash_infra shaders_lab
|
||||
|
||||
# Build windows (cross-compile)
|
||||
./fn run build_cpp_windows_bash_infra shaders_lab
|
||||
|
||||
# Tests del dominio gfx (puros, sin GL)
|
||||
g++ -std=c++17 -Icpp/functions -DDAG_CATALOG_TEST cpp/functions/gfx/dag_catalog.cpp -o /tmp/dag_catalog_test && /tmp/dag_catalog_test
|
||||
g++ -std=c++17 -Icpp/functions -DDAG_COMPILE_TEST cpp/functions/gfx/dag_compile.cpp cpp/functions/gfx/dag_catalog.cpp -o /tmp/dag_compile_test && /tmp/dag_compile_test
|
||||
g++ -std=c++17 -Icpp/functions -DCODE_TO_GENERATOR_TEST cpp/functions/gfx/code_to_generator.cpp cpp/functions/gfx/uniform_parser.cpp -o /tmp/code_to_generator_test && /tmp/code_to_generator_test
|
||||
g++ -std=c++17 -Icpp/functions -DUNIFORM_PARSER_TEST cpp/functions/gfx/uniform_parser.cpp -o /tmp/uniform_parser_test && /tmp/uniform_parser_test
|
||||
gcc -c -O2 -DSQLITE_THREADSAFE=1 cpp/vendor/sqlite3/sqlite3.c -o /tmp/sqlite3.o && \
|
||||
g++ -std=c++17 -Icpp/functions -Icpp/vendor/sqlite3 -DSHADERLAB_DB_TEST cpp/functions/gfx/shaderlab_db.cpp /tmp/sqlite3.o -lpthread -ldl -o /tmp/shaderlab_db_test && /tmp/shaderlab_db_test
|
||||
```
|
||||
|
||||
Cobertura de tests inline tras esta fase: **8 + 7 + 7 + 6 + 7 = 35 asserts** sobre `dag_catalog` (19 nodos), `dag_compile` (strict + baked), `code_to_generator`, `uniform_parser`, `shaderlab_db`.
|
||||
|
||||
Sync de binarios Windows (regla establecida en esta sesión):
|
||||
- `cpp/build/windows/apps/shaders_lab/shaders_lab.exe` (origen)
|
||||
- `apps/shaders_lab/shaders_lab.exe` (in-repo)
|
||||
- `/mnt/c/Users/lucas/Desktop/shaders_lab.exe` (Windows Desktop)
|
||||
- **NUNCA** copiar a `/mnt/c/Users/AdminLocal/`. Memoria persistente: `feedback_no_adminlocal.md`.
|
||||
|
||||
## Lo siguiente que pega
|
||||
|
||||
- Persistencia: `shaders_lab.db` local (SQLite). Guardar/cargar pipelines con nombre. Sync con `registry.db` global por tag (push selectivo de funciones GLSL "buenas" al registry para que aparezcan en `fn search`).
|
||||
- Mas nodos: warps (twirl, polar, kaleidoscope), ruidos reales (perlin, fbm, worley), SDFs (box, line, smooth_union), filtros de luma (threshold, levels, duotone).
|
||||
- Nodos custom definidos por el usuario: modal con editor GLSL del body + declaracion de params, persistencia en SQLite, aparicion automatica en la paleta.
|
||||
- Push selectivo al registry global: boton `Push to registry` que extrae el generator a `cpp/functions/gfx/<name>.{cpp,md}` con tag `shaders_lab` y dispara `fn index`.
|
||||
- Listado / borrado de generators custom desde la UI (hoy solo via DB directa).
|
||||
- Persistencia de pipelines con nombre.
|
||||
- Mas nodos: warps (twirl, polar, kaleidoscope), perlin/fbm reales, SDFs, filtros de luma.
|
||||
- Save as Op (1 input `a`) y Save as Blend (2 inputs).
|
||||
- Crossfade A↔B: tercer canvas que mezcla Canvas Code y Canvas DAG con un slider.
|
||||
- Cliente Claude: chat con tool use (`search_registry`, `apply_shader`, `save_function`).
|
||||
- Integracion VJ: Spout/Syphon/NDI para mandar el output a Resolume/OBS.
|
||||
|
||||
Documentacion de exploraciones aparcadas (no en backlog inmediato):
|
||||
- `NEXT_STEPS_DATA_TYPES.md` — extensiones del DAG: pins tipados, texturas, SDF/raymarch, multi-pass, geometria 3D.
|
||||
- `NEXT_STEPS_BORDERLESS_WINDOW.md` — quitar la titlebar del SO y mover min/max/close al `MainMenuBar` ImGui.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
@@ -141,18 +296,55 @@ uniform int u_preview_target; // -1 = real Output; 0..15 = render out_<i>
|
||||
|
||||
`dag_uniforms_apply` sube `u_params[16]` cada frame antes del draw del Canvas DAG. `dag_previews_render` rebinde el FBO de cada nodo abierto y setea `u_preview_target` antes de cada draw.
|
||||
|
||||
## Layout sugerido
|
||||
## Layouts
|
||||
|
||||
Al primer arranque las ventanas se apilan; arrastra por el titulo a los splits del dockspace. ImGui persiste el layout en `imgui.ini` junto al binario.
|
||||
ImGui persiste el layout actual en `imgui.ini` junto al binario (autosave). Ademas, el menu **Layouts** permite tener varios layouts guardados con nombre:
|
||||
|
||||
Disposicion comoda:
|
||||
- Mueve los paneles donde quieras.
|
||||
- `Layouts > Save current as...` y dale un nombre (ej. "Coding", "DAG mode", "Showcase").
|
||||
- Cambia el layout, guarda otro.
|
||||
- `Layouts > <nombre>` para saltar; el activo se marca con `* `.
|
||||
- `Layouts > Delete > <nombre>` para borrar.
|
||||
- `Layouts > Reset to default` reabre todos los paneles y limpia el marker.
|
||||
|
||||
Los layouts guardados viven en la tabla `ui_layouts` de `shaders_lab.db`.
|
||||
|
||||
Disposicion comoda al primer arranque:
|
||||
- `Code` y `DAG Pipeline` ocupan la fila superior.
|
||||
- `Canvas Code` y `Canvas DAG` ocupan la fila inferior, lado a lado.
|
||||
- `Functions` y `Controls` van a un lateral.
|
||||
- `Generated GLSL` minimizado o en pestana junto a `Controls`.
|
||||
|
||||
El menu **View** togglea cada panel individualmente (mismo `bool*` que la X de la ventana).
|
||||
|
||||
## Notas de cross-compile
|
||||
|
||||
- `gl_loader` resuelve simbolos OpenGL 2.0+ con `wglGetProcAddress` en Windows; en Linux es no-op (`GL_GLEXT_PROTOTYPES`).
|
||||
- `WIN32_EXECUTABLE TRUE` en `CMakeLists.txt` evita la consola al lanzar el .exe.
|
||||
- Vendor de imgui-node-editor cuesta ~1MB en el binario final (~18 MB total).
|
||||
|
||||
## Notas — Settings + iconos (sesion 2026-04-25)
|
||||
|
||||
- `app_menubar` ahora añade automaticamente un tercer item `Settings...` junto a `View` y `Layouts`. Click abre la ventana flotante de `app_settings` (Display: toggle FPS overlay; Typography: combo de fuente Karla/Roboto/DroidSans/Cousine + slider de tamaño 10..32 px). Persiste en `app_settings.ini` junto a `shaders_lab.exe`.
|
||||
- Defaults: DroidSans 15 px, FPS overlay off (antes hardcoded ON dentro del panel `Controls`).
|
||||
- Removida la llamada explicita `fps_overlay()` del panel `Controls` — ahora se respeta el toggle de Settings.
|
||||
- Removidos los `.cpp` de `fps_overlay`, `panel_menu`, `layouts_menu`, `app_menubar` del `CMakeLists.txt` — viven en `fn_framework` para evitar multiple-definition. Solo `layout_storage_sqlite.cpp` sigue listado explicitamente.
|
||||
- 5 TTFs (Karla / Roboto / DroidSans / Cousine / Tabler) copiadas junto al exe via `add_imgui_app` post-build.
|
||||
|
||||
Para añadir secciones propias de settings:
|
||||
|
||||
```cpp
|
||||
// En main.cpp antes de fn::run_app:
|
||||
fn_ui::settings_window_add_section("shader_compiler", "Shader compiler", []{
|
||||
ImGui::Checkbox("Auto-compile on save", &g_auto_compile);
|
||||
ImGui::SliderInt("Debounce (ms)", &g_debounce_ms, 50, 2000);
|
||||
});
|
||||
// Aparece debajo de Display/Typography. Persistencia propia (puede ir en
|
||||
// shaders_lab.db, tabla ui_settings).
|
||||
```
|
||||
|
||||
## Lo siguiente que pega
|
||||
|
||||
- Ejemplo concreto de seccion extra de settings: `auto-compile on save` + `debounce_ms` registrados desde `main.cpp` y persistidos en una tabla `ui_settings` en `shaders_lab.db`.
|
||||
- Auditar hex UTF-8 (`"\x..\x.."`) o emojis Unicode hardcoded en uniform_panel, dag_panel, dag_node_editor → migrar a `TI_*` de `core/icons_tabler.h`.
|
||||
- Rebuild Windows + sync: `cmake --build cpp/build/windows --target shaders_lab && cp cpp/build/windows/apps/shaders_lab/{shaders_lab.exe,*.ttf} /mnt/c/Users/lucas/Desktop/apps/shaders_lab/`.
|
||||
|
||||
Binary file not shown.
+60
-4
@@ -80,27 +80,82 @@ endif()
|
||||
target_link_libraries(imgui PUBLIC ${PLATFORM_LIBS})
|
||||
|
||||
# --- Framework ---
|
||||
# Incluye tokens.cpp (identidad visual Mantine dark + indigo), icon_font.cpp
|
||||
# (Karla/Roboto/... + Tabler), app_settings.cpp (persistencia y ventana de
|
||||
# settings) y fps_overlay.cpp (overlay opcional). Ver cpp/DESIGN_SYSTEM.md
|
||||
add_library(fn_framework STATIC
|
||||
framework/app_base.cpp
|
||||
functions/core/tokens.cpp
|
||||
functions/core/icon_font.cpp
|
||||
functions/core/app_settings.cpp
|
||||
functions/core/fps_overlay.cpp
|
||||
functions/core/panel_menu.cpp
|
||||
functions/core/layouts_menu.cpp
|
||||
functions/core/app_menubar.cpp
|
||||
)
|
||||
target_include_directories(fn_framework PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/framework
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/functions
|
||||
)
|
||||
# FN_CPP_ROOT permite que icon_font.cpp localice vendor/tabler-icons/tabler-icons.ttf
|
||||
# en builds de desarrollo desde el repo (en deploys, la TTF se copia junto al exe).
|
||||
target_compile_definitions(fn_framework PUBLIC
|
||||
FN_CPP_ROOT="${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
)
|
||||
target_link_libraries(fn_framework PUBLIC imgui implot)
|
||||
if(TRACY_ENABLE)
|
||||
target_link_libraries(fn_framework PUBLIC tracy)
|
||||
endif()
|
||||
|
||||
# --- Macro for creating ImGui apps ---
|
||||
# Capturamos la raiz del modulo cpp/ para que add_imgui_app la use desde
|
||||
# subdirectorios (donde CMAKE_CURRENT_SOURCE_DIR apunta al app, no al root).
|
||||
set(FN_CPP_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE INTERNAL "fn_registry cpp root")
|
||||
|
||||
function(add_imgui_app target)
|
||||
add_executable(${target} ${ARGN})
|
||||
target_link_libraries(${target} PRIVATE fn_framework)
|
||||
target_include_directories(${target} PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/functions
|
||||
${FN_CPP_ROOT_DIR}/functions
|
||||
)
|
||||
# Copia las fuentes junto al ejecutable para deploys autonomos (sin
|
||||
# FN_CPP_ROOT en runtime). 4 TTFs vectoriales para el menu Settings + Tabler
|
||||
# para los iconos TI_*.
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${FN_CPP_ROOT_DIR}/vendor/imgui/misc/fonts/Karla-Regular.ttf
|
||||
$<TARGET_FILE_DIR:${target}>/Karla-Regular.ttf
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${FN_CPP_ROOT_DIR}/vendor/imgui/misc/fonts/Roboto-Medium.ttf
|
||||
$<TARGET_FILE_DIR:${target}>/Roboto-Medium.ttf
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${FN_CPP_ROOT_DIR}/vendor/imgui/misc/fonts/DroidSans.ttf
|
||||
$<TARGET_FILE_DIR:${target}>/DroidSans.ttf
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${FN_CPP_ROOT_DIR}/vendor/imgui/misc/fonts/Cousine-Regular.ttf
|
||||
$<TARGET_FILE_DIR:${target}>/Cousine-Regular.ttf
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${FN_CPP_ROOT_DIR}/vendor/tabler-icons/tabler-icons.ttf
|
||||
$<TARGET_FILE_DIR:${target}>/tabler-icons.ttf
|
||||
VERBATIM
|
||||
)
|
||||
endfunction()
|
||||
|
||||
# --- SQLite3 (shared by every app that uses it) ---
|
||||
# System on Linux, vendored amalgamation on Windows cross-compile.
|
||||
find_package(SQLite3 QUIET)
|
||||
if(NOT SQLite3_FOUND AND NOT TARGET sqlite3_vendored)
|
||||
set(SQLITE3_AMALG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sqlite3)
|
||||
add_library(sqlite3_vendored STATIC ${SQLITE3_AMALG_DIR}/sqlite3.c)
|
||||
target_include_directories(sqlite3_vendored PUBLIC ${SQLITE3_AMALG_DIR})
|
||||
target_compile_definitions(sqlite3_vendored PRIVATE
|
||||
SQLITE_THREADSAFE=1
|
||||
SQLITE_ENABLE_FTS5
|
||||
SQLITE_ENABLE_JSON1
|
||||
)
|
||||
add_library(SQLite::SQLite3 ALIAS sqlite3_vendored)
|
||||
endif()
|
||||
|
||||
# --- Function libraries (headers for composition) ---
|
||||
# Functions are compiled as part of apps that use them via add_imgui_app.
|
||||
# Each function is a .h/.cpp pair included by the app's CMakeLists.txt.
|
||||
@@ -115,9 +170,10 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/shaders_lab/CMakeLists.txt)
|
||||
add_subdirectory(apps/shaders_lab)
|
||||
endif()
|
||||
|
||||
# --- Primitives Gallery ---
|
||||
# Activado solo si la app esta presente Y todos sus deps tambien (button, toolbar...
|
||||
# son sources untracked en este worktree). Forzar con FN_BUILD_GALLERY=ON.
|
||||
# --- Primitives Gallery (catalogo visual de primitivos core/viz/gfx) ---
|
||||
# Algunos deps del gallery (button.cpp, toolbar.cpp, modal_dialog.cpp, etc.)
|
||||
# todavia no estan tracked en master — son WIP del usuario en cpp/functions/core/.
|
||||
# Para construir el gallery: -DFN_BUILD_GALLERY=ON (requiere tener esos sources en disco).
|
||||
if(FN_BUILD_GALLERY AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/primitives_gallery/CMakeLists.txt)
|
||||
add_subdirectory(apps/primitives_gallery)
|
||||
endif()
|
||||
|
||||
@@ -4,5 +4,5 @@ add_imgui_app(chart_demo
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/fps_overlay.cpp
|
||||
# fps_overlay vive en fn_framework
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include "viz/scatter_plot.h"
|
||||
#include "viz/bar_chart.h"
|
||||
#include "viz/heatmap.h"
|
||||
#include "core/fps_overlay.h"
|
||||
#include "core/app_menubar.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
@@ -41,7 +41,9 @@ static void init_data() {
|
||||
|
||||
static void render() {
|
||||
init_data();
|
||||
fps_overlay();
|
||||
|
||||
// MainMenuBar (solo Settings — chart_demo no tiene paneles toggleables)
|
||||
fn_ui::app_menubar(nullptr, 0, nullptr);
|
||||
|
||||
// Full-window dockspace
|
||||
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
|
||||
|
||||
@@ -14,12 +14,15 @@ add_imgui_app(shaders_lab
|
||||
${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/core/fps_overlay.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/gfx/shaderlab_db.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/gfx/code_to_generator.cpp
|
||||
# fps_overlay, panel_menu, layouts_menu, app_menubar ya viven en fn_framework
|
||||
${CMAKE_SOURCE_DIR}/functions/core/layout_storage_sqlite.cpp
|
||||
)
|
||||
target_include_directories(shaders_lab PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
target_link_libraries(shaders_lab PRIVATE imgui_node_editor)
|
||||
target_link_libraries(shaders_lab PRIVATE imgui_node_editor SQLite::SQLite3)
|
||||
|
||||
if(WIN32)
|
||||
# GUI app: sin consola al lanzar (subsystem:windows / -mwindows)
|
||||
|
||||
+247
-29
@@ -12,18 +12,42 @@
|
||||
#include "gfx/dag_node_editor.h"
|
||||
#include "gfx/dag_palette.h"
|
||||
#include "gfx/dag_node_previews.h"
|
||||
#include "core/fps_overlay.h"
|
||||
#include "seed_shaders.h"
|
||||
#include "gfx/code_to_generator.h"
|
||||
#include "gfx/shaderlab_db.h"
|
||||
#include "core/panel_menu.h"
|
||||
#include "core/layouts_menu.h"
|
||||
#include "core/app_menubar.h"
|
||||
#include "core/layout_storage_sqlite.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
static fn::gfx::ShaderCanvas g_canvas_code;
|
||||
static fn::gfx::ShaderCanvas g_canvas_dag;
|
||||
|
||||
static std::string g_source = PLASMA;
|
||||
// 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";
|
||||
|
||||
static std::string g_source = CODE_PLACEHOLDER;
|
||||
static std::string g_code_err;
|
||||
static int g_code_err_line = -1;
|
||||
static std::chrono::steady_clock::time_point g_code_last_edit;
|
||||
@@ -37,6 +61,28 @@ static std::string g_dag_err;
|
||||
static int g_dag_err_line = -1;
|
||||
static bool g_dag_dirty = true;
|
||||
|
||||
// ── 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;
|
||||
|
||||
// ── Layouts (named ImGui ini snapshots persisted in shaders_lab.db) ───────
|
||||
static fn_ui::LayoutCallbacks g_layout_cb;
|
||||
static std::string g_pending_layout_blob; // applied at start of next frame
|
||||
static std::string g_pending_layout_name; // becomes active_name after apply
|
||||
|
||||
// ── 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;
|
||||
|
||||
static void compile_code() {
|
||||
auto r = fn::gfx::compile_fragment(g_source);
|
||||
if (r.ok) {
|
||||
@@ -69,13 +115,7 @@ static void mark_code_dirty() {
|
||||
g_code_dirty = true;
|
||||
}
|
||||
|
||||
static void load_preset(const char* src) {
|
||||
g_source = src;
|
||||
mark_code_dirty();
|
||||
}
|
||||
|
||||
static void ensure_dag_default() {
|
||||
// Seed with a Plasma connected to an Output node.
|
||||
if (g_pipeline.empty()) {
|
||||
const fn::gfx::DagNodeDef* plasma = fn::gfx::dag_find("plasma");
|
||||
if (plasma) {
|
||||
@@ -86,7 +126,6 @@ static void ensure_dag_default() {
|
||||
g_pipeline.push_back(s);
|
||||
}
|
||||
}
|
||||
// Ensure there is always an Output node at the end.
|
||||
bool has_output = false;
|
||||
for (const auto& s : g_pipeline) {
|
||||
const fn::gfx::DagNodeDef* d = fn::gfx::dag_find(s.name);
|
||||
@@ -115,7 +154,82 @@ static void draw_err(const std::string& msg, int line) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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.)
|
||||
if (!g_pending_layout_blob.empty()) {
|
||||
ImGui::LoadIniSettingsFromMemory(g_pending_layout_blob.c_str(),
|
||||
g_pending_layout_blob.size());
|
||||
g_layout_cb.active_name = g_pending_layout_name;
|
||||
g_pending_layout_blob.clear();
|
||||
g_pending_layout_name.clear();
|
||||
}
|
||||
|
||||
if (!g_canvas_code.initialized) fn::gfx::canvas_init(g_canvas_code);
|
||||
if (!g_canvas_dag.initialized) fn::gfx::canvas_init(g_canvas_dag);
|
||||
|
||||
@@ -134,13 +248,56 @@ static void render() {
|
||||
|
||||
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
|
||||
|
||||
// --- Menubar (View + Layouts) ---
|
||||
fn_ui::PanelToggle toggles[] = {
|
||||
{"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},
|
||||
};
|
||||
fn_ui::app_menubar(toggles, std::size(toggles), &g_layout_cb);
|
||||
|
||||
// --- Code window ---
|
||||
if (ImGui::Begin("Code")) {
|
||||
if (ImGui::Button("Plasma")) { load_preset(PLASMA); }
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Circle")) { load_preset(CIRCLE); }
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Checker")) { load_preset(CHECKER); }
|
||||
if (g_show_code) {
|
||||
if (ImGui::Begin("Code", &g_show_code)) {
|
||||
if (ImGui::Button("Save as generator...")) {
|
||||
g_save_modal_open = true;
|
||||
g_save_err.clear();
|
||||
ImGui::OpenPopup("save_as_generator");
|
||||
}
|
||||
|
||||
if (ImGui::BeginPopupModal("save_as_generator", &g_save_modal_open,
|
||||
ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
ImGui::Text("Guardar shader actual como nodo Gen del DAG.");
|
||||
ImGui::Spacing();
|
||||
ImGui::InputText("name (snake_case)", g_save_name, sizeof(g_save_name));
|
||||
ImGui::InputText("label", g_save_label, sizeof(g_save_label));
|
||||
ImGui::InputTextMultiline("description", g_save_desc, sizeof(g_save_desc),
|
||||
ImVec2(380, 60));
|
||||
ImGui::InputText("tags (CSV)", g_save_tags, sizeof(g_save_tags));
|
||||
|
||||
if (!g_save_err.empty()) {
|
||||
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", g_save_err.c_str());
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Save", ImVec2(120, 0))) {
|
||||
g_save_err = save_current_as_generator();
|
||||
if (g_save_err.empty()) {
|
||||
g_save_modal_open = false;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
||||
g_save_modal_open = false;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
float footer_h = g_code_err.empty() ? 0.0f : ImGui::GetTextLineHeightWithSpacing() + 8.0f;
|
||||
@@ -160,84 +317,145 @@ static void render() {
|
||||
draw_err(g_code_err, g_code_err_line);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// --- DAG Pipeline window ---
|
||||
if (ImGui::Begin("DAG Pipeline")) {
|
||||
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 ---
|
||||
if (ImGui::Begin("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 (ImGui::Begin("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();
|
||||
}
|
||||
|
||||
// Render per-node previews (only nodes with preview_open=true)
|
||||
if (g_canvas_dag.program) {
|
||||
fn::gfx::dag_previews_render(g_pipeline, g_canvas_dag.program);
|
||||
}
|
||||
|
||||
// --- Controls window (Code uniforms) ---
|
||||
if (ImGui::Begin("Controls")) {
|
||||
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);
|
||||
}
|
||||
ImGui::Spacing();
|
||||
fps_overlay();
|
||||
// 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 (ImGui::Begin("Functions")) {
|
||||
if (g_show_functions) {
|
||||
if (ImGui::Begin("Functions", &g_show_functions)) {
|
||||
fn::gfx::dag_palette();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// --- Generated GLSL window (DAG compiled output, read-only) ---
|
||||
if (ImGui::Begin("Generated GLSL")) {
|
||||
if (g_dag_glsl.empty()) {
|
||||
// --- 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*>(g_dag_glsl.c_str()),
|
||||
g_dag_glsl.size() + 1,
|
||||
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 on the same shaders_lab.db connection.
|
||||
sqlite3* db = fn::gfx::shaderlab_db_handle();
|
||||
fn_ui::layout_storage_init(db);
|
||||
|
||||
g_layout_cb.list = [db]() {
|
||||
return fn_ui::layout_storage_list(db);
|
||||
};
|
||||
g_layout_cb.on_apply = [db](const std::string& name) {
|
||||
std::string blob = fn_ui::layout_storage_load_blob(db, name);
|
||||
if (!blob.empty()) {
|
||||
g_pending_layout_blob = std::move(blob);
|
||||
g_pending_layout_name = name;
|
||||
}
|
||||
};
|
||||
g_layout_cb.on_save = [db](const std::string& name) {
|
||||
size_t size = 0;
|
||||
const char* blob = ImGui::SaveIniSettingsToMemory(&size);
|
||||
if (blob && size > 0) {
|
||||
fn_ui::layout_storage_save(db, name, std::string(blob, size));
|
||||
g_layout_cb.active_name = name;
|
||||
}
|
||||
};
|
||||
g_layout_cb.on_delete = [db](const std::string& name) {
|
||||
fn_ui::layout_storage_delete(db, name);
|
||||
if (g_layout_cb.active_name == name) g_layout_cb.active_name.clear();
|
||||
};
|
||||
g_layout_cb.on_reset = []() {
|
||||
// Default reset: open every panel and clear active layout marker.
|
||||
// The actual dock layout is whatever ImGui rebuilt on first launch.
|
||||
g_show_code = g_show_dag = g_show_canvas_c = g_show_canvas_d =
|
||||
g_show_controls = g_show_functions = g_show_generated = true;
|
||||
g_layout_cb.active_name.clear();
|
||||
};
|
||||
|
||||
fn::AppConfig cfg;
|
||||
cfg.title = "shaders_lab";
|
||||
cfg.width = 1600;
|
||||
cfg.height = 900;
|
||||
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::gfx::shaderlab_db_close();
|
||||
return rc;
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// GLSL 330 fragment shader bodies (no #version, no out, no uniform declarations).
|
||||
// compile_fragment() prepends those automatically.
|
||||
|
||||
static const char* PLASMA = R"glsl(
|
||||
uniform float u_speed; // @slider min=0.1 max=5 default=1
|
||||
uniform vec3 u_color; // @color default=0.5,0.2,0.8
|
||||
|
||||
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";
|
||||
|
||||
static const char* CIRCLE = R"glsl(
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / u_resolution;
|
||||
vec2 center = vec2(0.5);
|
||||
float t = u_time;
|
||||
|
||||
// Animated center
|
||||
center += vec2(sin(t * 0.7), cos(t * 0.5)) * 0.2;
|
||||
|
||||
float d = length(uv - center);
|
||||
|
||||
// Concentric rings
|
||||
float rings = sin(d * 40.0 - t * 3.0) * 0.5 + 0.5;
|
||||
|
||||
// Radial glow
|
||||
float glow = exp(-d * 4.0);
|
||||
|
||||
vec3 col = mix(
|
||||
vec3(0.05, 0.1, 0.3),
|
||||
vec3(0.2, 0.7, 1.0),
|
||||
rings * glow + glow * 0.4
|
||||
);
|
||||
|
||||
fragColor = vec4(col, 1.0);
|
||||
}
|
||||
)glsl";
|
||||
|
||||
static const char* CHECKER = R"glsl(
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / u_resolution;
|
||||
float t = u_time;
|
||||
|
||||
// Animated scale and rotation
|
||||
float scale = 8.0 + sin(t * 0.4) * 3.0;
|
||||
float angle = t * 0.2;
|
||||
float ca = cos(angle), sa = sin(angle);
|
||||
vec2 p = uv - 0.5;
|
||||
p = vec2(ca * p.x - sa * p.y, sa * p.x + ca * p.y);
|
||||
p = p * scale + 0.5;
|
||||
|
||||
vec2 cell = floor(p);
|
||||
float checker = mod(cell.x + cell.y, 2.0);
|
||||
|
||||
// Color gradient per cell
|
||||
float hue = fract((cell.x + cell.y) * 0.1 + t * 0.05);
|
||||
vec3 col_a = vec3(hue, 0.7, 0.9);
|
||||
vec3 col_b = vec3(fract(hue + 0.5), 0.5, 0.7);
|
||||
|
||||
// Simple HSV to RGB
|
||||
vec3 col = mix(col_b, col_a, checker);
|
||||
// hue is already [0,1], apply saturation/value manually
|
||||
float h = col.x * 6.0;
|
||||
int i = int(h);
|
||||
float f = h - float(i);
|
||||
float p2 = col.z * (1.0 - col.y);
|
||||
float q2 = col.z * (1.0 - col.y * f);
|
||||
float t2 = col.z * (1.0 - col.y * (1.0 - f));
|
||||
vec3 rgb;
|
||||
if (i == 0) rgb = vec3(col.z, t2, p2);
|
||||
else if (i == 1) rgb = vec3(q2, col.z, p2);
|
||||
else if (i == 2) rgb = vec3(p2, col.z, t2);
|
||||
else if (i == 3) rgb = vec3(p2, q2, col.z);
|
||||
else if (i == 4) rgb = vec3(t2, p2, col.z);
|
||||
else rgb = vec3(col.z, p2, q2);
|
||||
|
||||
fragColor = vec4(rgb, 1.0);
|
||||
}
|
||||
)glsl";
|
||||
@@ -4,6 +4,10 @@
|
||||
#include "imgui_impl_glfw.h"
|
||||
#include "imgui_impl_opengl3.h"
|
||||
#include "implot.h"
|
||||
#include "core/tokens.h"
|
||||
#include "core/icon_font.h"
|
||||
#include "core/app_settings.h"
|
||||
#include "core/fps_overlay.h"
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <cstdio>
|
||||
@@ -52,11 +56,34 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
|
||||
// Lee app_settings.ini (font_id, font_size_px, show_fps) antes de cargar
|
||||
// fuentes. Si no existe el .ini, los defaults se aplican.
|
||||
fn_ui::settings_load();
|
||||
|
||||
// Texto vectorial (Karla / Roboto / DroidSans / Cousine, segun settings)
|
||||
// + iconos Tabler mergeados al mismo tamaño en el mismo ImFont.
|
||||
fn_ui::load_fonts_from_settings();
|
||||
|
||||
// ImGui 1.92+ usa style.FontSizeBase como tamaño activo (escalable sin
|
||||
// rebuild de atlas). Inicializa al valor del .ini para que el primer
|
||||
// frame ya respete el setting.
|
||||
{
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
style.FontSizeBase = fn_ui::settings().font_size_px;
|
||||
style._NextFrameFontSizeBase = style.FontSizeBase;
|
||||
}
|
||||
|
||||
if (config.viewports) {
|
||||
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
|
||||
}
|
||||
|
||||
ImGui::StyleColorsDark();
|
||||
// Identidad visual — ver cpp/DESIGN_SYSTEM.md
|
||||
switch (config.theme) {
|
||||
case ThemeMode::FnDark: fn_tokens::apply_dark_theme(); break;
|
||||
case ThemeMode::ImGuiDark: ImGui::StyleColorsDark(); break;
|
||||
case ThemeMode::ImGuiLight: ImGui::StyleColorsLight(); break;
|
||||
case ThemeMode::None: break;
|
||||
}
|
||||
|
||||
// When viewports are enabled, tweak WindowRounding/WindowBg so
|
||||
// platform windows look consistent with the main window
|
||||
@@ -78,12 +105,35 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tamaño de fuente: aplica via style.FontSizeBase cada frame. Cambios
|
||||
// se ven al instante (ImGui 1.92+ escala el atlas dinamicamente, no
|
||||
// hace falta rebuild).
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
if (style.FontSizeBase != fn_ui::settings().font_size_px) {
|
||||
style.FontSizeBase = fn_ui::settings().font_size_px;
|
||||
style._NextFrameFontSizeBase = style.FontSizeBase; // FIXME-ImGui hack
|
||||
}
|
||||
|
||||
// Cambio de fuente (font_id): rebuild atlas. ImGui_ImplOpenGL3
|
||||
// refresca la GPU texture via UpdateTexture en RenderDrawData.
|
||||
if (fn_ui::settings_consume_font_dirty()) {
|
||||
fn_ui::load_fonts_from_settings();
|
||||
}
|
||||
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplGlfw_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
render_fn();
|
||||
|
||||
// Ventana de Settings (no-op si esta cerrada).
|
||||
fn_ui::settings_window_render();
|
||||
|
||||
// FPS overlay si esta activado en Settings.
|
||||
if (fn_ui::settings().show_fps) {
|
||||
fps_overlay();
|
||||
}
|
||||
|
||||
ImGui::Render();
|
||||
int display_w, display_h;
|
||||
glfwGetFramebufferSize(window, &display_w, &display_h);
|
||||
@@ -107,6 +157,9 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Persiste settings al exit (idempotente con auto-saves del menu).
|
||||
fn_ui::settings_save();
|
||||
|
||||
// Cleanup
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplGlfw_Shutdown();
|
||||
|
||||
@@ -4,15 +4,24 @@
|
||||
|
||||
namespace fn {
|
||||
|
||||
// Modos de tema para run_app.
|
||||
enum class ThemeMode {
|
||||
FnDark, // Identidad del registry (Mantine v9 dark + indigo). DEFAULT.
|
||||
ImGuiDark, // Tema estandar de ImGui (ImGui::StyleColorsDark).
|
||||
ImGuiLight,
|
||||
None, // No tocar el ImGuiStyle — la app lo configura.
|
||||
};
|
||||
|
||||
struct AppConfig {
|
||||
const char* title = "fn_registry";
|
||||
int width = 1280;
|
||||
int height = 720;
|
||||
bool vsync = true;
|
||||
bool viewports = false; // Enable multi-viewport: ImGui windows become real OS windows
|
||||
float bg_r = 0.1f;
|
||||
float bg_g = 0.1f;
|
||||
float bg_b = 0.1f;
|
||||
ThemeMode theme = ThemeMode::FnDark; // Identidad visual unificada por defecto
|
||||
float bg_r = 0.102f; // fn_tokens::colors::bg (dark.7 #1A1B1E)
|
||||
float bg_g = 0.106f;
|
||||
float bg_b = 0.118f;
|
||||
};
|
||||
|
||||
// Run an ImGui application. The render_fn is called every frame
|
||||
|
||||
@@ -28,3 +28,21 @@ output: "Renderiza el overlay de FPS en el frame ImGui actual"
|
||||
Muestra FPS y frametime (ms) en una ventana semi-transparente en la esquina superior derecha.
|
||||
|
||||
Si se compila con `TRACY_ENABLE`, incluye un `ZoneScoped` para profiling con Tracy.
|
||||
|
||||
## Notas — Auto-render via app_settings (sesion 2026-04-25)
|
||||
|
||||
A partir de la integracion del sistema `app_settings`, las apps NO deben llamar `fps_overlay()` directamente. `fn::run_app` consulta `fn_ui::settings().show_fps` cada frame, y si esta activo invoca `fps_overlay()` automaticamente despues del `render_fn` de la app. El usuario controla el toggle desde `Settings... → Display → Show FPS overlay`, persistido en `app_settings.ini` junto al exe.
|
||||
|
||||
Anti-patron eliminado:
|
||||
|
||||
```cpp
|
||||
// ❌ Hardcoded — ignora el toggle de Settings, siempre se ve
|
||||
static void render() {
|
||||
fps_overlay();
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ No hace falta llamarla. fn::run_app la dispara segun settings.
|
||||
```
|
||||
|
||||
Si una app no usa `fn::run_app` (raro), debe llamar manualmente `fps_overlay()` segun su criterio.
|
||||
|
||||
+127
-35
@@ -1,50 +1,142 @@
|
||||
#include "tokens.h"
|
||||
|
||||
#if __has_include("implot.h")
|
||||
#include "implot.h"
|
||||
#define FN_HAS_IMPLOT 1
|
||||
#endif
|
||||
|
||||
namespace fn_tokens {
|
||||
|
||||
void apply_dark_theme() {
|
||||
ImGuiStyle& s = ImGui::GetStyle();
|
||||
|
||||
// Colors
|
||||
s.Colors[ImGuiCol_WindowBg] = colors::bg;
|
||||
s.Colors[ImGuiCol_ChildBg] = colors::surface;
|
||||
s.Colors[ImGuiCol_PopupBg] = colors::surface;
|
||||
s.Colors[ImGuiCol_FrameBg] = colors::surface;
|
||||
s.Colors[ImGuiCol_FrameBgHovered] = colors::surface_hover;
|
||||
s.Colors[ImGuiCol_FrameBgActive] = colors::surface_hover;
|
||||
s.Colors[ImGuiCol_Border] = colors::border;
|
||||
s.Colors[ImGuiCol_BorderShadow] = ImVec4(0, 0, 0, 0);
|
||||
s.Colors[ImGuiCol_Text] = colors::text;
|
||||
s.Colors[ImGuiCol_TextDisabled] = colors::text_dim;
|
||||
s.Colors[ImGuiCol_Button] = colors::primary;
|
||||
s.Colors[ImGuiCol_ButtonHovered] = colors::primary_hover;
|
||||
s.Colors[ImGuiCol_ButtonActive] = colors::primary;
|
||||
s.Colors[ImGuiCol_Header] = colors::surface_hover;
|
||||
s.Colors[ImGuiCol_HeaderHovered] = colors::primary;
|
||||
s.Colors[ImGuiCol_HeaderActive] = colors::primary_hover;
|
||||
s.Colors[ImGuiCol_Tab] = colors::surface;
|
||||
s.Colors[ImGuiCol_TabHovered] = colors::primary_hover;
|
||||
s.Colors[ImGuiCol_TabActive] = colors::primary;
|
||||
s.Colors[ImGuiCol_Separator] = colors::border;
|
||||
s.Colors[ImGuiCol_TableHeaderBg] = colors::surface_hover;
|
||||
s.Colors[ImGuiCol_TableBorderLight]= colors::border;
|
||||
s.Colors[ImGuiCol_TableBorderStrong]= colors::border;
|
||||
// ---------- Colors ----------
|
||||
auto& c = s.Colors;
|
||||
c[ImGuiCol_WindowBg] = colors::bg;
|
||||
c[ImGuiCol_ChildBg] = colors::surface;
|
||||
c[ImGuiCol_PopupBg] = colors::surface;
|
||||
c[ImGuiCol_MenuBarBg] = colors::surface;
|
||||
c[ImGuiCol_FrameBg] = colors::surface;
|
||||
c[ImGuiCol_FrameBgHovered] = colors::surface_hover;
|
||||
c[ImGuiCol_FrameBgActive] = colors::surface_active;
|
||||
|
||||
// Radius
|
||||
s.FrameRounding = radius::sm;
|
||||
s.ChildRounding = radius::md;
|
||||
s.WindowRounding = radius::md;
|
||||
s.PopupRounding = radius::md;
|
||||
s.TabRounding = radius::sm;
|
||||
s.GrabRounding = radius::sm;
|
||||
c[ImGuiCol_TitleBg] = colors::bg;
|
||||
c[ImGuiCol_TitleBgActive] = colors::surface;
|
||||
c[ImGuiCol_TitleBgCollapsed] = colors::bg;
|
||||
|
||||
c[ImGuiCol_Border] = colors::border;
|
||||
c[ImGuiCol_BorderShadow] = ImVec4(0, 0, 0, 0);
|
||||
|
||||
c[ImGuiCol_Text] = colors::text;
|
||||
c[ImGuiCol_TextDisabled] = colors::text_dim;
|
||||
c[ImGuiCol_TextSelectedBg] = ImVec4(colors::primary.x, colors::primary.y, colors::primary.z, 0.35f);
|
||||
|
||||
c[ImGuiCol_Button] = colors::primary;
|
||||
c[ImGuiCol_ButtonHovered] = colors::primary_hover;
|
||||
c[ImGuiCol_ButtonActive] = colors::primary_active;
|
||||
|
||||
c[ImGuiCol_CheckMark] = colors::primary_light;
|
||||
c[ImGuiCol_SliderGrab] = colors::primary;
|
||||
c[ImGuiCol_SliderGrabActive] = colors::primary_hover;
|
||||
|
||||
c[ImGuiCol_Header] = colors::surface_hover;
|
||||
c[ImGuiCol_HeaderHovered] = colors::primary;
|
||||
c[ImGuiCol_HeaderActive] = colors::primary_hover;
|
||||
|
||||
c[ImGuiCol_Tab] = colors::surface;
|
||||
c[ImGuiCol_TabHovered] = colors::primary_hover;
|
||||
c[ImGuiCol_TabActive] = colors::primary;
|
||||
c[ImGuiCol_TabUnfocused] = colors::bg;
|
||||
c[ImGuiCol_TabUnfocusedActive] = colors::surface_hover;
|
||||
|
||||
c[ImGuiCol_Separator] = colors::border;
|
||||
c[ImGuiCol_SeparatorHovered] = colors::primary_light;
|
||||
c[ImGuiCol_SeparatorActive] = colors::primary;
|
||||
|
||||
c[ImGuiCol_ResizeGrip] = ImVec4(colors::primary.x, colors::primary.y, colors::primary.z, 0.25f);
|
||||
c[ImGuiCol_ResizeGripHovered] = colors::primary_hover;
|
||||
c[ImGuiCol_ResizeGripActive] = colors::primary_active;
|
||||
|
||||
c[ImGuiCol_ScrollbarBg] = colors::bg;
|
||||
c[ImGuiCol_ScrollbarGrab] = colors::surface_hover;
|
||||
c[ImGuiCol_ScrollbarGrabHovered] = colors::surface_active;
|
||||
c[ImGuiCol_ScrollbarGrabActive] = colors::border_strong;
|
||||
|
||||
c[ImGuiCol_TableHeaderBg] = colors::surface_hover;
|
||||
c[ImGuiCol_TableBorderLight] = colors::border;
|
||||
c[ImGuiCol_TableBorderStrong] = colors::border;
|
||||
c[ImGuiCol_TableRowBg] = ImVec4(0, 0, 0, 0);
|
||||
c[ImGuiCol_TableRowBgAlt] = ImVec4(colors::surface.x, colors::surface.y, colors::surface.z, 0.40f);
|
||||
|
||||
c[ImGuiCol_DockingPreview] = ImVec4(colors::primary.x, colors::primary.y, colors::primary.z, 0.60f);
|
||||
c[ImGuiCol_DockingEmptyBg] = colors::bg;
|
||||
|
||||
c[ImGuiCol_PlotLines] = colors::primary_light;
|
||||
c[ImGuiCol_PlotLinesHovered] = colors::primary;
|
||||
c[ImGuiCol_PlotHistogram] = colors::primary_light;
|
||||
c[ImGuiCol_PlotHistogramHovered] = colors::primary;
|
||||
|
||||
c[ImGuiCol_DragDropTarget] = colors::primary_light;
|
||||
c[ImGuiCol_NavHighlight] = colors::primary_light;
|
||||
c[ImGuiCol_NavWindowingHighlight]= ImVec4(1, 1, 1, 0.70f);
|
||||
c[ImGuiCol_NavWindowingDimBg] = ImVec4(0, 0, 0, 0.40f);
|
||||
c[ImGuiCol_ModalWindowDimBg] = ImVec4(0, 0, 0, 0.55f);
|
||||
|
||||
// ---------- Radius ----------
|
||||
s.WindowRounding = radius::md;
|
||||
s.ChildRounding = radius::md;
|
||||
s.PopupRounding = radius::md;
|
||||
s.FrameRounding = radius::md;
|
||||
s.GrabRounding = radius::sm;
|
||||
s.ScrollbarRounding = radius::md;
|
||||
s.TabRounding = radius::sm;
|
||||
|
||||
// Spacing
|
||||
// ---------- Spacing & padding ----------
|
||||
s.WindowPadding = ImVec2(spacing::md, spacing::md);
|
||||
s.FramePadding = ImVec2(spacing::sm, spacing::xs + 2.0f);
|
||||
s.CellPadding = ImVec2(spacing::sm, spacing::xs);
|
||||
s.ItemSpacing = ImVec2(spacing::sm, spacing::sm);
|
||||
s.ItemInnerSpacing = ImVec2(spacing::xs, spacing::xs);
|
||||
s.FramePadding = ImVec2(spacing::sm, spacing::xs + 2.0f);
|
||||
s.WindowPadding = ImVec2(spacing::md, spacing::md);
|
||||
s.CellPadding = ImVec2(spacing::sm, spacing::xs);
|
||||
s.IndentSpacing = spacing::lg;
|
||||
s.ScrollbarSize = 14.0f;
|
||||
s.GrabMinSize = 12.0f;
|
||||
|
||||
// ---------- Borders ----------
|
||||
s.WindowBorderSize = 1.0f;
|
||||
s.ChildBorderSize = 1.0f;
|
||||
s.PopupBorderSize = 1.0f;
|
||||
s.FrameBorderSize = 0.0f; // Mantine no pinta borde en frames (usa bg sutil)
|
||||
s.TabBorderSize = 0.0f;
|
||||
|
||||
// ---------- Viewports (multi-OS-window) ----------
|
||||
// Cuando ImGuiConfigFlags_ViewportsEnable esta activo las ventanas pueden
|
||||
// salirse del monitor principal; en ese caso WindowRounding debe ser 0
|
||||
// y alpha 1.0 para que los compositors no muestren esquinas raras.
|
||||
// app_base gestiona este caso; aqui dejamos los valores para single-viewport.
|
||||
|
||||
// ---------- ImPlot ----------
|
||||
#ifdef FN_HAS_IMPLOT
|
||||
if (ImPlot::GetCurrentContext() != nullptr) {
|
||||
ImPlotStyle& p = ImPlot::GetStyle();
|
||||
p.Colors[ImPlotCol_FrameBg] = colors::surface;
|
||||
p.Colors[ImPlotCol_PlotBg] = colors::bg;
|
||||
p.Colors[ImPlotCol_PlotBorder] = colors::border;
|
||||
p.Colors[ImPlotCol_LegendBg] = colors::surface;
|
||||
p.Colors[ImPlotCol_LegendBorder] = colors::border;
|
||||
p.Colors[ImPlotCol_LegendText] = colors::text;
|
||||
p.Colors[ImPlotCol_TitleText] = colors::text;
|
||||
p.Colors[ImPlotCol_InlayText] = colors::text_muted;
|
||||
p.Colors[ImPlotCol_AxisText] = colors::text_muted;
|
||||
p.Colors[ImPlotCol_AxisGrid] = colors::border;
|
||||
p.Colors[ImPlotCol_AxisTick] = colors::border;
|
||||
p.Colors[ImPlotCol_AxisBg] = ImVec4(0, 0, 0, 0);
|
||||
p.Colors[ImPlotCol_Selection] = colors::primary_light;
|
||||
p.Colors[ImPlotCol_Crosshairs] = colors::text_muted;
|
||||
p.PlotPadding = ImVec2(spacing::sm, spacing::sm);
|
||||
p.LabelPadding = ImVec2(spacing::xs, spacing::xs);
|
||||
p.LegendPadding = ImVec2(spacing::sm, spacing::sm);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace fn_tokens
|
||||
|
||||
+44
-27
@@ -1,37 +1,49 @@
|
||||
#pragma once
|
||||
#include "imgui.h"
|
||||
|
||||
// Design tokens — colores, spacing, radius, font-size.
|
||||
// Inspirados en el DESIGN_SYSTEM de @fn_library (Mantine v9 dark + indigo primary).
|
||||
// Reemplaza hardcode disperso de ImVec4(...) por constantes semánticas.
|
||||
// Design tokens — identidad visual unica para todas las apps C++ del registry.
|
||||
// Alineados 1:1 con @fn_library (Mantine v9 dark + indigo primary).
|
||||
// Ver cpp/DESIGN_SYSTEM.md para la especificacion completa.
|
||||
//
|
||||
// Equivalencias frontend -> C++:
|
||||
// createTheme({ primaryColor: 'indigo', primaryShade: {dark:4},
|
||||
// defaultRadius: 'md', defaultColorScheme: 'dark' })
|
||||
// se corresponde con los valores de este header.
|
||||
|
||||
namespace fn_tokens {
|
||||
|
||||
namespace colors {
|
||||
// Primary (indigo-inspired, Mantine indigo.6)
|
||||
constexpr ImVec4 primary {0.25f, 0.37f, 0.85f, 1.0f};
|
||||
constexpr ImVec4 primary_hover {0.30f, 0.42f, 0.90f, 1.0f};
|
||||
// Primary — Mantine indigo (primaryShade dark = 4)
|
||||
// indigo.4 = #748FFC indigo.5 = #5C7CFA indigo.6 = #4C6EF5 indigo.7 = #4263EB
|
||||
constexpr ImVec4 primary {0.298f, 0.431f, 0.961f, 1.0f}; // indigo.6 (base)
|
||||
constexpr ImVec4 primary_hover {0.361f, 0.486f, 0.980f, 1.0f}; // indigo.5
|
||||
constexpr ImVec4 primary_light {0.455f, 0.561f, 0.988f, 1.0f}; // indigo.4 (dark mode accent)
|
||||
constexpr ImVec4 primary_active {0.259f, 0.388f, 0.922f, 1.0f}; // indigo.7
|
||||
|
||||
// Semantic
|
||||
constexpr ImVec4 success {0.13f, 0.70f, 0.42f, 1.0f};
|
||||
constexpr ImVec4 warning {0.95f, 0.60f, 0.20f, 1.0f};
|
||||
constexpr ImVec4 error {0.87f, 0.26f, 0.30f, 1.0f};
|
||||
constexpr ImVec4 info {0.22f, 0.55f, 0.95f, 1.0f};
|
||||
// Semantic — colores Mantine oficiales
|
||||
constexpr ImVec4 success {0.251f, 0.753f, 0.341f, 1.0f}; // green.6 #40C057
|
||||
constexpr ImVec4 warning {0.980f, 0.690f, 0.020f, 1.0f}; // yellow.6 #FAB005
|
||||
constexpr ImVec4 error {0.980f, 0.322f, 0.322f, 1.0f}; // red.6 #FA5252
|
||||
constexpr ImVec4 info {0.133f, 0.545f, 0.902f, 1.0f}; // blue.6 #228BE6
|
||||
|
||||
// Background (dark by default — matches DESIGN_SYSTEM.md §2 & §8)
|
||||
constexpr ImVec4 bg {0.08f, 0.08f, 0.10f, 1.0f}; // window bg
|
||||
constexpr ImVec4 surface {0.12f, 0.12f, 0.15f, 1.0f}; // panels/cards
|
||||
constexpr ImVec4 surface_hover {0.16f, 0.16f, 0.20f, 1.0f};
|
||||
|
||||
// Text
|
||||
constexpr ImVec4 text {0.95f, 0.95f, 0.95f, 1.0f};
|
||||
constexpr ImVec4 text_muted {0.60f, 0.60f, 0.65f, 1.0f};
|
||||
constexpr ImVec4 text_dim {0.40f, 0.40f, 0.45f, 1.0f};
|
||||
// Surfaces — escala dark Mantine (oscuro a claro: dark.9 -> dark.0)
|
||||
constexpr ImVec4 bg {0.102f, 0.106f, 0.118f, 1.0f}; // dark.7 #1A1B1E body bg
|
||||
constexpr ImVec4 surface {0.145f, 0.149f, 0.169f, 1.0f}; // dark.6 #25262B Paper/Card
|
||||
constexpr ImVec4 surface_hover {0.173f, 0.180f, 0.200f, 1.0f}; // dark.5 #2C2E33
|
||||
constexpr ImVec4 surface_active{0.216f, 0.227f, 0.251f, 1.0f}; // dark.4 #373A40
|
||||
|
||||
// Border
|
||||
constexpr ImVec4 border {0.20f, 0.20f, 0.25f, 1.0f};
|
||||
constexpr ImVec4 border {0.216f, 0.227f, 0.251f, 1.0f}; // dark.4 #373A40
|
||||
constexpr ImVec4 border_strong {0.361f, 0.373f, 0.400f, 1.0f}; // dark.3 #5C5F66
|
||||
|
||||
// Text (escala dark inversa: dark.0 mas claro)
|
||||
constexpr ImVec4 text {0.757f, 0.761f, 0.773f, 1.0f}; // dark.0 #C1C2C5 texto primario
|
||||
constexpr ImVec4 text_muted {0.565f, 0.573f, 0.588f, 1.0f}; // dark.2 #909296 subtitulos
|
||||
constexpr ImVec4 text_dim {0.361f, 0.373f, 0.400f, 1.0f}; // dark.3 #5C5F66 disabled
|
||||
}
|
||||
|
||||
// Spacing — adaptado para ImGui (densidad mayor que CSS).
|
||||
// Mantine usa 10/12/16/20/32 px, aqui densificamos al estilo TUI clasico.
|
||||
namespace spacing {
|
||||
constexpr float xs = 4.0f;
|
||||
constexpr float sm = 8.0f;
|
||||
@@ -40,12 +52,14 @@ namespace spacing {
|
||||
constexpr float xl = 24.0f;
|
||||
}
|
||||
|
||||
// Radius — mapeo directo Mantine (defaultRadius: 'md' = 8px).
|
||||
namespace radius {
|
||||
constexpr float none = 0.0f;
|
||||
constexpr float sm = 3.0f;
|
||||
constexpr float md = 5.0f;
|
||||
constexpr float lg = 8.0f;
|
||||
constexpr float xl = 12.0f;
|
||||
constexpr float xs = 2.0f;
|
||||
constexpr float sm = 4.0f;
|
||||
constexpr float md = 8.0f; // default para Paper/Card/Button/Input
|
||||
constexpr float lg = 12.0f;
|
||||
constexpr float xl = 16.0f;
|
||||
}
|
||||
|
||||
namespace font_size {
|
||||
@@ -57,8 +71,11 @@ namespace font_size {
|
||||
constexpr float xxl = 32.0f;
|
||||
}
|
||||
|
||||
// Aplica los tokens al ImGuiStyle global. Llamar una vez al arrancar la app,
|
||||
// después de ImGui::CreateContext() y antes del primer frame.
|
||||
// Aplica la identidad visual al ImGuiStyle y a los estilos de ImPlot (si
|
||||
// esta enlazado). Se invoca automaticamente desde fn::run_app() (app_base.h)
|
||||
// salvo que AppConfig::theme sea ImGuiDark/ImGuiLight/None.
|
||||
// Idempotente: se puede llamar varias veces sin efectos secundarios.
|
||||
// No desactiva ninguna feature de ImGui (docking, viewports, etc).
|
||||
void apply_dark_theme();
|
||||
|
||||
} // namespace fn_tokens
|
||||
|
||||
@@ -3,10 +3,10 @@ name: tokens
|
||||
kind: component
|
||||
lang: cpp
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
version: "2.0.0"
|
||||
purity: pure
|
||||
signature: "namespace fn_tokens { namespace colors/spacing/radius/font_size { constexpr ... }; void apply_dark_theme(); }"
|
||||
description: "Design tokens (colors, spacing, radius, font_size) para dashboards ImGui. Inspirados en @fn_library (Mantine v9) — dark theme con indigo primary. Reemplaza hardcode de ImVec4(...) por constantes semánticas."
|
||||
description: "Design tokens (colors, spacing, radius, font_size) para apps ImGui. Valores exactos de Mantine v9 dark + indigo — identidad unica del registry. Aplicados por defecto via fn::run_app. Ver cpp/DESIGN_SYSTEM.md."
|
||||
tags: [imgui, theme, tokens, colors, spacing, radius, dark, design-system]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
@@ -31,28 +31,39 @@ Design tokens para todos los dashboards ImGui del registry. Traducción del DESI
|
||||
|
||||
| Namespace | Valores |
|
||||
|-----------|---------|
|
||||
| `fn_tokens::colors` | `primary`, `primary_hover`, `success`, `warning`, `error`, `info`, `bg`, `surface`, `surface_hover`, `text`, `text_muted`, `text_dim`, `border` |
|
||||
| `fn_tokens::spacing` | `xs=4`, `sm=8`, `md=12`, `lg=16`, `xl=24` (px) |
|
||||
| `fn_tokens::radius` | `none=0`, `sm=3`, `md=5`, `lg=8`, `xl=12` (px) |
|
||||
| `fn_tokens::font_size` | `xs=10`, `sm=12`, `md=14`, `lg=18`, `xl=24`, `xxl=32` (px) |
|
||||
| `fn_tokens::colors` | `primary` (indigo.6), `primary_hover` (indigo.5), `primary_light` (indigo.4), `primary_active` (indigo.7), `success` (green.6), `warning` (yellow.6), `error` (red.6), `info` (blue.6), `bg` (dark.7), `surface` (dark.6), `surface_hover` (dark.5), `surface_active` (dark.4), `border` (dark.4), `border_strong` (dark.3), `text` (dark.0), `text_muted` (dark.2), `text_dim` (dark.3) |
|
||||
| `fn_tokens::spacing` | `xs=4`, `sm=8`, `md=12`, `lg=16`, `xl=24` (densificado respecto a CSS Mantine) |
|
||||
| `fn_tokens::radius` | `none=0`, `xs=2`, `sm=4`, `md=8`, `lg=12`, `xl=16` (md = defaultRadius Mantine) |
|
||||
| `fn_tokens::font_size` | `xs=10`, `sm=12`, `md=14`, `lg=18`, `xl=24`, `xxl=32` |
|
||||
|
||||
## Uso
|
||||
## Uso normal: nada, lo aplica el framework
|
||||
|
||||
```cpp
|
||||
#include "app_base.h"
|
||||
fn::run_app({.title="app", .width=1400, .height=900}, render);
|
||||
// run_app llama a fn_tokens::apply_dark_theme() una vez. No hace falta mas.
|
||||
```
|
||||
|
||||
## Uso en componentes
|
||||
|
||||
```cpp
|
||||
#include "core/tokens.h"
|
||||
|
||||
// Al arrancar la app (una vez, después de ImGui::CreateContext)
|
||||
fn_tokens::apply_dark_theme();
|
||||
|
||||
// En componentes
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
||||
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));
|
||||
ImGui::PopStyleColor();
|
||||
```
|
||||
|
||||
## Detalles de `apply_dark_theme()`
|
||||
|
||||
- Valores alineados 1:1 con Mantine v9 dark (dark.0-9) + indigo (4-7).
|
||||
- Aplica ~50 ImGuiCol_*, rounding (Window/Child/Popup/Frame/Grab/Scrollbar/Tab), paddings y bordes.
|
||||
- Si `ImPlot` esta linkado, tambien estiliza su frame/plot/axis/legend.
|
||||
- No toca `io.ConfigFlags`, backends, fuentes ni contexto — las capacidades de ImGui (docking, viewports, nav teclado) quedan intactas.
|
||||
- Idempotente.
|
||||
|
||||
## Notas
|
||||
|
||||
- **Dark by default** como en el DESIGN_SYSTEM (§2, §8). Si algún día queremos light, se añade `apply_light_theme()`.
|
||||
- Los valores semánticos (success/warning/error/info) se usan en `badge`, `kpi_card` para deltas, y gráficos de estado.
|
||||
- **No duplicar** estas constantes en componentes — siempre importar de aquí. Si se detecta un `ImVec4` hardcodeado en un componente del registry es candidato a migrar.
|
||||
- Compatible con `plot_theme_cpp_core` (para ImPlot charts) — los colors del palette se pueden derivar de estos tokens si se quiere coherencia total.
|
||||
- **No duplicar** constantes en componentes — importar siempre de aqui. Si detectas un `ImVec4(...)` hardcoded en el registry, es candidato a migrar.
|
||||
- Para temas alternativos (dev/debug) usar `fn::AppConfig::theme = fn::ThemeMode::ImGuiDark`.
|
||||
- Compatible con `plot_theme_cpp_core` — `apply_dark_theme` ya setea los `ImPlotCol_*` base.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#include "gfx/dag_catalog.h"
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
namespace fn::gfx {
|
||||
|
||||
static const std::vector<DagNodeDef>& build_catalog() {
|
||||
static std::vector<DagNodeDef>& mutable_catalog() {
|
||||
static std::vector<DagNodeDef> catalog = []() {
|
||||
std::vector<DagNodeDef> v;
|
||||
|
||||
@@ -15,8 +16,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
|
||||
n.desc = "color constante";
|
||||
n.kind = DagKind::Gen;
|
||||
n.num_inputs = 0;
|
||||
n.param_names = {"r", "g", "b", ""};
|
||||
n.param_defaults = {0.35f, 0.25f, 0.55f, 0.0f};
|
||||
n.param_names = {"r", "g", "b"};
|
||||
n.param_defaults = {0.35f, 0.25f, 0.55f};
|
||||
n.controls = {
|
||||
{ DagControl::Kind::Color, "color", {0, 1, 2}, 0.0f, 1.0f, 0.0f },
|
||||
};
|
||||
@@ -36,8 +37,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
|
||||
n.desc = "gradiente direccional";
|
||||
n.kind = DagKind::Gen;
|
||||
n.num_inputs = 0;
|
||||
n.param_names = {"angle", "hue", "", ""};
|
||||
n.param_defaults = {0.8f, 0.5f, 0.0f, 0.0f};
|
||||
n.param_names = {"angle", "hue"};
|
||||
n.param_defaults = {0.8f, 0.5f};
|
||||
n.controls = {
|
||||
{ DagControl::Kind::Slider, "angulo", {0, -1, -1}, 0.0f, 6.2832f, 0.01f },
|
||||
{ DagControl::Kind::Slider, "tono", {1, -1, -1}, 0.0f, 1.0f, 0.01f },
|
||||
@@ -61,8 +62,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
|
||||
n.desc = "onda trigonometrica";
|
||||
n.kind = DagKind::Gen;
|
||||
n.num_inputs = 0;
|
||||
n.param_names = {"speed", "scale", "", ""};
|
||||
n.param_defaults = {1.0f, 2.0f, 0.0f, 0.0f};
|
||||
n.param_names = {"speed", "scale"};
|
||||
n.param_defaults = {1.0f, 2.0f};
|
||||
n.controls = {
|
||||
{ DagControl::Kind::Slider, "velocidad", {0, -1, -1}, 0.0f, 3.0f, 0.01f },
|
||||
{ DagControl::Kind::Slider, "escala", {1, -1, -1}, 0.5f, 10.0f, 0.1f },
|
||||
@@ -112,8 +113,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
|
||||
n.desc = "1 - rgb";
|
||||
n.kind = DagKind::Op;
|
||||
n.num_inputs = 1;
|
||||
n.param_names = {"", "", "", ""};
|
||||
n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
n.param_names = {};
|
||||
n.param_defaults = {};
|
||||
n.controls = {};
|
||||
n.body_glsl = [](int /*idx*/) -> std::string {
|
||||
return " return vec4(1.0 - a.rgb, a.a);";
|
||||
@@ -129,8 +130,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
|
||||
n.desc = "pow(rgb, gamma)";
|
||||
n.kind = DagKind::Op;
|
||||
n.num_inputs = 1;
|
||||
n.param_names = {"gamma", "", "", ""};
|
||||
n.param_defaults = {1.0f, 0.0f, 0.0f, 0.0f};
|
||||
n.param_names = {"gamma"};
|
||||
n.param_defaults = {1.0f};
|
||||
n.controls = {
|
||||
{ DagControl::Kind::Slider, "gamma", {0, -1, -1}, 0.1f, 4.0f, 0.01f },
|
||||
};
|
||||
@@ -150,8 +151,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
|
||||
n.desc = "rotar matiz";
|
||||
n.kind = DagKind::Op;
|
||||
n.num_inputs = 1;
|
||||
n.param_names = {"h", "", "", ""};
|
||||
n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
n.param_names = {"h"};
|
||||
n.param_defaults = {0.0f};
|
||||
n.controls = {
|
||||
{ DagControl::Kind::Slider, "h", {0, -1, -1}, 0.0f, 1.0f, 0.01f },
|
||||
};
|
||||
@@ -178,8 +179,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
|
||||
n.desc = "interpolacion mix(a, b, t)";
|
||||
n.kind = DagKind::Blend;
|
||||
n.num_inputs = 2;
|
||||
n.param_names = {"t", "", "", ""};
|
||||
n.param_defaults = {0.5f, 0.0f, 0.0f, 0.0f};
|
||||
n.param_names = {"t"};
|
||||
n.param_defaults = {0.5f};
|
||||
n.controls = {
|
||||
{ DagControl::Kind::Slider, "t", {0, -1, -1}, 0.0f, 1.0f, 0.01f },
|
||||
};
|
||||
@@ -199,8 +200,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
|
||||
n.desc = "a * b";
|
||||
n.kind = DagKind::Blend;
|
||||
n.num_inputs = 2;
|
||||
n.param_names = {"", "", "", ""};
|
||||
n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
n.param_names = {};
|
||||
n.param_defaults = {};
|
||||
n.controls = {};
|
||||
n.body_glsl = [](int /*idx*/) -> std::string {
|
||||
return " return vec4(a.rgb * b.rgb, a.a);";
|
||||
@@ -216,8 +217,8 @@ static const std::vector<DagNodeDef>& build_catalog() {
|
||||
n.desc = "1 - (1-a)(1-b)";
|
||||
n.kind = DagKind::Blend;
|
||||
n.num_inputs = 2;
|
||||
n.param_names = {"", "", "", ""};
|
||||
n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
n.param_names = {};
|
||||
n.param_defaults = {};
|
||||
n.controls = {};
|
||||
n.body_glsl = [](int /*idx*/) -> std::string {
|
||||
return " return vec4(1.0 - (1.0 - a.rgb) * (1.0 - b.rgb), a.a);";
|
||||
@@ -225,6 +226,253 @@ static const std::vector<DagNodeDef>& build_catalog() {
|
||||
v.push_back(std::move(n));
|
||||
}
|
||||
|
||||
// ── Gen: checker ──────────────────────────────────────────────
|
||||
{
|
||||
DagNodeDef n;
|
||||
n.name = "checker";
|
||||
n.label = "checker";
|
||||
n.desc = "tablero animado";
|
||||
n.kind = DagKind::Gen;
|
||||
n.num_inputs = 0;
|
||||
n.param_names = {"scale", "rotation", "hue", "speed"};
|
||||
n.param_defaults = {8.0f, 0.0f, 0.55f, 0.2f};
|
||||
n.controls = {
|
||||
{ DagControl::Kind::Slider, "escala", {0, -1, -1}, 1.0f, 32.0f, 0.1f },
|
||||
{ DagControl::Kind::Slider, "rotacion", {1, -1, -1}, 0.0f, 6.2832f, 0.01f },
|
||||
{ DagControl::Kind::Slider, "tono", {2, -1, -1}, 0.0f, 1.0f, 0.01f },
|
||||
{ DagControl::Kind::Slider, "velocidad",{3, -1, -1}, 0.0f, 3.0f, 0.01f },
|
||||
};
|
||||
n.body_glsl = [](int idx) -> std::string {
|
||||
std::string i = std::to_string(idx);
|
||||
return " vec4 p = u_params[" + i + "];\n"
|
||||
" float ang = p.y + u_time * p.w * 0.2;\n"
|
||||
" float ca = cos(ang), sa = sin(ang);\n"
|
||||
" vec2 q = uv - 0.5;\n"
|
||||
" q = vec2(ca*q.x - sa*q.y, sa*q.x + ca*q.y) * p.x + 0.5;\n"
|
||||
" vec2 cell = floor(q);\n"
|
||||
" float c = mod(cell.x + cell.y, 2.0);\n"
|
||||
" vec3 ca_col = 0.5 + 0.5 * cos(6.28318 * (p.z + vec3(0.0, 0.33, 0.67)));\n"
|
||||
" vec3 cb_col = 0.5 + 0.5 * cos(6.28318 * (p.z + 0.5 + vec3(0.0, 0.33, 0.67)));\n"
|
||||
" return vec4(mix(cb_col, ca_col, c), 1.0);";
|
||||
};
|
||||
v.push_back(std::move(n));
|
||||
}
|
||||
|
||||
// ── Gen: stripes ──────────────────────────────────────────────
|
||||
{
|
||||
DagNodeDef n;
|
||||
n.name = "stripes";
|
||||
n.label = "stripes";
|
||||
n.desc = "bandas direccionales";
|
||||
n.kind = DagKind::Gen;
|
||||
n.num_inputs = 0;
|
||||
n.param_names = {"angle", "freq", "phase_speed", "hue"};
|
||||
n.param_defaults = {0.785f, 12.0f, 1.0f, 0.6f};
|
||||
n.controls = {
|
||||
{ DagControl::Kind::Slider, "angulo", {0, -1, -1}, 0.0f, 6.2832f, 0.01f },
|
||||
{ DagControl::Kind::Slider, "frecuencia", {1, -1, -1}, 1.0f, 64.0f, 0.5f },
|
||||
{ DagControl::Kind::Slider, "velocidad", {2, -1, -1}, 0.0f, 5.0f, 0.01f },
|
||||
{ DagControl::Kind::Slider, "tono", {3, -1, -1}, 0.0f, 1.0f, 0.01f },
|
||||
};
|
||||
n.body_glsl = [](int idx) -> std::string {
|
||||
std::string i = std::to_string(idx);
|
||||
return " vec4 p = u_params[" + i + "];\n"
|
||||
" vec2 dir = vec2(cos(p.x), sin(p.x));\n"
|
||||
" float t = dot(uv - 0.5, dir) * p.y + u_time * p.z;\n"
|
||||
" float s = 0.5 + 0.5 * sin(t);\n"
|
||||
" vec3 col = 0.5 + 0.5 * cos(6.28318 * (p.w + s + vec3(0.0, 0.33, 0.67)));\n"
|
||||
" return vec4(col, 1.0);";
|
||||
};
|
||||
v.push_back(std::move(n));
|
||||
}
|
||||
|
||||
// ── Gen: dots ─────────────────────────────────────────────────
|
||||
{
|
||||
DagNodeDef n;
|
||||
n.name = "dots";
|
||||
n.label = "dots";
|
||||
n.desc = "rejilla de puntos";
|
||||
n.kind = DagKind::Gen;
|
||||
n.num_inputs = 0;
|
||||
n.param_names = {"scale", "radius", "soft", "hue"};
|
||||
n.param_defaults = {16.0f, 0.3f, 0.05f, 0.7f};
|
||||
n.controls = {
|
||||
{ DagControl::Kind::Slider, "escala", {0, -1, -1}, 1.0f, 64.0f, 0.5f },
|
||||
{ DagControl::Kind::Slider, "radio", {1, -1, -1}, 0.0f, 0.5f, 0.01f },
|
||||
{ DagControl::Kind::Slider, "suavidad", {2, -1, -1}, 0.001f, 0.2f, 0.001f },
|
||||
{ DagControl::Kind::Slider, "tono", {3, -1, -1}, 0.0f, 1.0f, 0.01f },
|
||||
};
|
||||
n.body_glsl = [](int idx) -> std::string {
|
||||
std::string i = std::to_string(idx);
|
||||
return " vec4 p = u_params[" + i + "];\n"
|
||||
" float aspect = u_resolution.x / u_resolution.y;\n"
|
||||
" vec2 q = vec2((uv.x - 0.5) * aspect, uv.y - 0.5) * p.x;\n"
|
||||
" vec2 cell = fract(q) - 0.5;\n"
|
||||
" float d = length(cell) - p.y;\n"
|
||||
" float fill = smoothstep(p.z, -p.z, d);\n"
|
||||
" vec3 col = 0.5 + 0.5 * cos(6.28318 * (p.w + vec3(0.0, 0.33, 0.67)));\n"
|
||||
" return vec4(col * fill, 1.0);";
|
||||
};
|
||||
v.push_back(std::move(n));
|
||||
}
|
||||
|
||||
// ── Gen: rings ────────────────────────────────────────────────
|
||||
{
|
||||
DagNodeDef n;
|
||||
n.name = "rings";
|
||||
n.label = "rings";
|
||||
n.desc = "anillos concentricos";
|
||||
n.kind = DagKind::Gen;
|
||||
n.num_inputs = 0;
|
||||
n.param_names = {"cx", "cy", "freq", "speed"};
|
||||
n.param_defaults = {0.0f, 0.0f, 30.0f, 2.0f};
|
||||
n.controls = {
|
||||
{ DagControl::Kind::XY, "centro", {0, 1, -1}, -0.5f, 0.5f, 0.01f },
|
||||
{ DagControl::Kind::Slider, "frecuencia",{2, -1, -1}, 1.0f, 100.0f, 0.5f },
|
||||
{ DagControl::Kind::Slider, "velocidad", {3, -1, -1}, 0.0f, 10.0f, 0.05f },
|
||||
};
|
||||
n.body_glsl = [](int idx) -> std::string {
|
||||
std::string i = std::to_string(idx);
|
||||
return " vec4 p = u_params[" + i + "];\n"
|
||||
" float aspect = u_resolution.x / u_resolution.y;\n"
|
||||
" vec2 q = vec2((uv.x - 0.5) * aspect - p.x, uv.y - 0.5 - p.y);\n"
|
||||
" float d = length(q);\n"
|
||||
" float r = 0.5 + 0.5 * sin(d * p.z - u_time * p.w);\n"
|
||||
" return vec4(vec3(r), 1.0);";
|
||||
};
|
||||
v.push_back(std::move(n));
|
||||
}
|
||||
|
||||
// ── Gen: polar_rays ───────────────────────────────────────────
|
||||
{
|
||||
DagNodeDef n;
|
||||
n.name = "polar_rays";
|
||||
n.label = "polar rays";
|
||||
n.desc = "rayos radiales";
|
||||
n.kind = DagKind::Gen;
|
||||
n.num_inputs = 0;
|
||||
n.param_names = {"cx", "cy", "count", "speed"};
|
||||
n.param_defaults = {0.0f, 0.0f, 12.0f, 0.5f};
|
||||
n.controls = {
|
||||
{ DagControl::Kind::XY, "centro", {0, 1, -1}, -0.5f, 0.5f, 0.01f },
|
||||
{ DagControl::Kind::Slider, "rayos", {2, -1, -1}, 1.0f, 64.0f, 1.0f },
|
||||
{ DagControl::Kind::Slider, "velocidad", {3, -1, -1}, -3.0f, 3.0f, 0.01f },
|
||||
};
|
||||
n.body_glsl = [](int idx) -> std::string {
|
||||
std::string i = std::to_string(idx);
|
||||
return " vec4 p = u_params[" + i + "];\n"
|
||||
" float aspect = u_resolution.x / u_resolution.y;\n"
|
||||
" vec2 q = vec2((uv.x - 0.5) * aspect - p.x, uv.y - 0.5 - p.y);\n"
|
||||
" float a = atan(q.y, q.x);\n"
|
||||
" float r = 0.5 + 0.5 * sin(a * p.z + u_time * p.w);\n"
|
||||
" return vec4(vec3(r), 1.0);";
|
||||
};
|
||||
v.push_back(std::move(n));
|
||||
}
|
||||
|
||||
// ── Gen: noise_value ──────────────────────────────────────────
|
||||
{
|
||||
DagNodeDef n;
|
||||
n.name = "noise_value";
|
||||
n.label = "noise value";
|
||||
n.desc = "value noise 2D";
|
||||
n.kind = DagKind::Gen;
|
||||
n.num_inputs = 0;
|
||||
n.param_names = {"scale", "speed", "hue"};
|
||||
n.param_defaults = {6.0f, 0.3f, 0.5f};
|
||||
n.controls = {
|
||||
{ DagControl::Kind::Slider, "escala", {0, -1, -1}, 0.5f, 32.0f, 0.1f },
|
||||
{ DagControl::Kind::Slider, "velocidad", {1, -1, -1}, 0.0f, 3.0f, 0.01f },
|
||||
{ DagControl::Kind::Slider, "tono", {2, -1, -1}, 0.0f, 1.0f, 0.01f },
|
||||
};
|
||||
n.body_glsl = [](int idx) -> std::string {
|
||||
std::string i = std::to_string(idx);
|
||||
return " vec4 p = u_params[" + i + "];\n"
|
||||
" vec2 q = uv * p.x + u_time * p.y;\n"
|
||||
" vec2 fl = floor(q);\n"
|
||||
" vec2 fr = fract(q);\n"
|
||||
" fr = fr * fr * (3.0 - 2.0 * fr);\n"
|
||||
" float a = fract(sin(dot(fl + vec2(0.0, 0.0), vec2(12.9898, 78.233))) * 43758.5453);\n"
|
||||
" float b = fract(sin(dot(fl + vec2(1.0, 0.0), vec2(12.9898, 78.233))) * 43758.5453);\n"
|
||||
" float c = fract(sin(dot(fl + vec2(0.0, 1.0), vec2(12.9898, 78.233))) * 43758.5453);\n"
|
||||
" float d = fract(sin(dot(fl + vec2(1.0, 1.0), vec2(12.9898, 78.233))) * 43758.5453);\n"
|
||||
" float n = mix(mix(a, b, fr.x), mix(c, d, fr.x), fr.y);\n"
|
||||
" vec3 col = 0.5 + 0.5 * cos(6.28318 * (p.z + n + vec3(0.0, 0.33, 0.67)));\n"
|
||||
" return vec4(col, 1.0);";
|
||||
};
|
||||
v.push_back(std::move(n));
|
||||
}
|
||||
|
||||
// ── Gen: voronoi ──────────────────────────────────────────────
|
||||
{
|
||||
DagNodeDef n;
|
||||
n.name = "voronoi";
|
||||
n.label = "voronoi";
|
||||
n.desc = "celdas voronoi";
|
||||
n.kind = DagKind::Gen;
|
||||
n.num_inputs = 0;
|
||||
n.param_names = {"scale", "speed", "hue"};
|
||||
n.param_defaults = {8.0f, 0.5f, 0.4f};
|
||||
n.controls = {
|
||||
{ DagControl::Kind::Slider, "escala", {0, -1, -1}, 1.0f, 32.0f, 0.1f },
|
||||
{ DagControl::Kind::Slider, "velocidad", {1, -1, -1}, 0.0f, 3.0f, 0.01f },
|
||||
{ DagControl::Kind::Slider, "tono", {2, -1, -1}, 0.0f, 1.0f, 0.01f },
|
||||
};
|
||||
n.body_glsl = [](int idx) -> std::string {
|
||||
std::string i = std::to_string(idx);
|
||||
return " vec4 p = u_params[" + i + "];\n"
|
||||
" vec2 q = uv * p.x;\n"
|
||||
" vec2 fl = floor(q);\n"
|
||||
" vec2 fr = fract(q);\n"
|
||||
" float md = 1.0;\n"
|
||||
" for (int yy = -1; yy <= 1; yy++) {\n"
|
||||
" for (int xx = -1; xx <= 1; xx++) {\n"
|
||||
" vec2 nb = vec2(float(xx), float(yy));\n"
|
||||
" vec2 r = fract(sin(dot(fl + nb, vec2(127.1, 311.7))) * vec2(43758.5453, 22578.1459)) * vec2(1.0);\n"
|
||||
" vec2 pt = nb + 0.5 + 0.5 * sin(u_time * p.y + 6.2831 * r) - fr;\n"
|
||||
" md = min(md, dot(pt, pt));\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" float d = sqrt(md);\n"
|
||||
" vec3 col = 0.5 + 0.5 * cos(6.28318 * (p.z + d + vec3(0.0, 0.33, 0.67)));\n"
|
||||
" return vec4(col, 1.0);";
|
||||
};
|
||||
v.push_back(std::move(n));
|
||||
}
|
||||
|
||||
// ── Gen: truchet ──────────────────────────────────────────────
|
||||
{
|
||||
DagNodeDef n;
|
||||
n.name = "truchet";
|
||||
n.label = "truchet";
|
||||
n.desc = "patron truchet curvo";
|
||||
n.kind = DagKind::Gen;
|
||||
n.num_inputs = 0;
|
||||
n.param_names = {"scale", "thickness", "hue"};
|
||||
n.param_defaults = {10.0f, 0.15f, 0.3f};
|
||||
n.controls = {
|
||||
{ DagControl::Kind::Slider, "escala", {0, -1, -1}, 1.0f, 40.0f, 0.5f },
|
||||
{ DagControl::Kind::Slider, "grosor", {1, -1, -1}, 0.02f, 0.45f, 0.01f },
|
||||
{ DagControl::Kind::Slider, "tono", {2, -1, -1}, 0.0f, 1.0f, 0.01f },
|
||||
};
|
||||
n.body_glsl = [](int idx) -> std::string {
|
||||
std::string i = std::to_string(idx);
|
||||
return " vec4 p = u_params[" + i + "];\n"
|
||||
" vec2 q = uv * p.x;\n"
|
||||
" vec2 fl = floor(q);\n"
|
||||
" vec2 fr = fract(q);\n"
|
||||
" float h = fract(sin(dot(fl, vec2(12.9898, 78.233))) * 43758.5453);\n"
|
||||
" if (h > 0.5) fr.x = 1.0 - fr.x;\n"
|
||||
" float d1 = abs(length(fr) - 0.5);\n"
|
||||
" float d2 = abs(length(fr - 1.0) - 0.5);\n"
|
||||
" float d = min(d1, d2);\n"
|
||||
" float fill = 1.0 - smoothstep(p.y - 0.02, p.y, d);\n"
|
||||
" vec3 col = 0.5 + 0.5 * cos(6.28318 * (p.z + vec3(0.0, 0.33, 0.67)));\n"
|
||||
" return vec4(col * fill, 1.0);";
|
||||
};
|
||||
v.push_back(std::move(n));
|
||||
}
|
||||
|
||||
// ── Output (sink — drives fragColor) ─────────────────────────
|
||||
{
|
||||
DagNodeDef n;
|
||||
@@ -233,20 +481,21 @@ static const std::vector<DagNodeDef>& build_catalog() {
|
||||
n.desc = "canvas DAG output";
|
||||
n.kind = DagKind::Output;
|
||||
n.num_inputs = 1;
|
||||
n.param_names = {"", "", "", ""};
|
||||
n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
n.param_names = {};
|
||||
n.param_defaults = {};
|
||||
n.controls = {};
|
||||
n.body_glsl = [](int) -> std::string { return ""; };
|
||||
v.push_back(std::move(n));
|
||||
}
|
||||
|
||||
for (auto& n : v) n.is_builtin = true;
|
||||
return v;
|
||||
}();
|
||||
return catalog;
|
||||
}
|
||||
|
||||
const std::vector<DagNodeDef>& dag_catalog() {
|
||||
return build_catalog();
|
||||
return mutable_catalog();
|
||||
}
|
||||
|
||||
const DagNodeDef* dag_find(const std::string& name) {
|
||||
@@ -256,6 +505,30 @@ const DagNodeDef* dag_find(const std::string& name) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool dag_register_node(DagNodeDef def) {
|
||||
auto& cat = mutable_catalog();
|
||||
for (auto& n : cat) {
|
||||
if (n.name == def.name) {
|
||||
if (n.is_builtin) return false;
|
||||
n = std::move(def);
|
||||
n.is_builtin = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
def.is_builtin = false;
|
||||
cat.push_back(std::move(def));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool dag_unregister_node(const std::string& name) {
|
||||
auto& cat = mutable_catalog();
|
||||
auto it = std::find_if(cat.begin(), cat.end(),
|
||||
[&](const DagNodeDef& n){ return n.name == name && !n.is_builtin; });
|
||||
if (it == cat.end()) return false;
|
||||
cat.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace fn::gfx
|
||||
|
||||
#ifdef DAG_CATALOG_TEST
|
||||
@@ -319,13 +592,15 @@ int main() {
|
||||
assert(body.find("return") != std::string::npos);
|
||||
}
|
||||
|
||||
// 8. Control param indices stay within 0..3
|
||||
// 8. Control param indices stay within 0..param_count-1 of their node
|
||||
for (const auto& n : cat) {
|
||||
int pc = static_cast<int>(n.param_defaults.size());
|
||||
for (const auto& c : n.controls) {
|
||||
for (int idx : c.param_idx) {
|
||||
assert(idx >= -1 && idx < 4);
|
||||
assert(idx >= -1 && idx < pc);
|
||||
}
|
||||
}
|
||||
assert(n.param_names.size() == n.param_defaults.size());
|
||||
}
|
||||
|
||||
std::printf("dag_catalog: 8/8 asserts passed (%zu nodes)\n", cat.size());
|
||||
|
||||
@@ -4,8 +4,19 @@
|
||||
|
||||
namespace fn::gfx {
|
||||
|
||||
// Active catalog (built-in nodes + any user-registered ones).
|
||||
const std::vector<DagNodeDef>& dag_catalog();
|
||||
|
||||
// Look up a node by name. Returns nullptr if not present.
|
||||
const DagNodeDef* dag_find(const std::string& name);
|
||||
|
||||
// Add (or replace) a user-defined node. Returns false if `def.name` collides
|
||||
// with a built-in. Replacing an existing user node by same name is allowed.
|
||||
// Always sets def.is_builtin = false on the stored copy.
|
||||
bool dag_register_node(DagNodeDef def);
|
||||
|
||||
// Remove a user-defined node by name. Returns true if removed.
|
||||
// Built-in nodes cannot be removed.
|
||||
bool dag_unregister_node(const std::string& name);
|
||||
|
||||
} // namespace fn::gfx
|
||||
|
||||
@@ -44,3 +44,21 @@ output: "dag_catalog(): referencia const estable al vector de DagNodeDef (instan
|
||||
## Notas
|
||||
|
||||
Los cuerpos GLSL omiten las declaraciones de u_time, u_resolution, u_params — las proporciona el preamble de gl_shader::compile_fragment o compile_dag_to_glsl. El indice idx que recibe body_glsl es la posicion en el pipeline (para indexar u_params[idx]).
|
||||
|
||||
## Cambios 2026-04-25 (Fase 5 + Fase 7 shaders_lab)
|
||||
|
||||
Catálogo creció de 11 a **19 nodos**. Nuevos `Gen` (8): `checker`, `stripes`, `dots`, `rings`, `polar_rays`, `noise_value`, `voronoi`, `truchet`. Bug fix: `solid` ahora muestra label en su control Color (era invisible por `ImGuiColorEditFlags_NoLabel`).
|
||||
|
||||
API mutable y lifecycle (declarados en `dag_catalog.h`):
|
||||
- `dag_register_node(DagNodeDef def) -> bool`: añade o reemplaza un nodo user. Refuse si el nombre colisiona con un built-in. Setea `is_builtin = false` en el stored.
|
||||
- `dag_unregister_node(name) -> bool`: borra un user node. Built-ins están protegidos.
|
||||
- Flag `is_builtin` en `DagNodeDef` (ver `dag_types.h`). Built-ins se cargan en el constructor estático y nunca se tocan tras eso.
|
||||
|
||||
Layout de params:
|
||||
- `param_names`/`param_defaults` pasan de `array<*,4>` a `vector<*>`. Cada nodo declara la cantidad real de floats que necesita (sin padding cosmético).
|
||||
- `body_glsl` recibe `int base_vec4` (índice base en el array global), no el index del nodo. El compilador lo calcula vía `dag_param_layout`.
|
||||
- `body_glsl(idx)` semantically: where `idx` was the node index, now it is the vec4 base. Bodies que originalmente hacían `vec4 p = u_params[i]; ...; p.x ... p.w` siguen funcionando porque cada nodo built-in cabe en 1 vec4. Generators custom de Code → DAG (`code_to_generator`) reciben `__BASE__` como placeholder y la lambda lo sustituye en runtime con el valor real.
|
||||
|
||||
`body_glsl(int base_vec4)` retorna string con cuerpo de la función `vec4 node_<i>(vec4 a?, vec4 b?, ..., vec2 uv)`. Los inputs llegan como params `a`,`b`,`c`,`d` según `num_inputs`; `uv` siempre presente.
|
||||
|
||||
Tests: 8/8 (19 nodos, invariantes por kind + 1 control_idx in-bounds + name uniqueness).
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
#include "gfx/dag_compile.h"
|
||||
#include "gfx/dag_catalog.h"
|
||||
#include <algorithm>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
|
||||
namespace fn::gfx {
|
||||
|
||||
static constexpr int MAX_NODES = 16;
|
||||
static constexpr int MAX_NODES = 16;
|
||||
static constexpr int MAX_PARAM_VEC4S = 64; // 256 floats — enough for 16 nodes × ~16 floats each
|
||||
|
||||
std::vector<int> dag_param_layout(const std::vector<DagStep>& pipeline) {
|
||||
std::vector<int> base(pipeline.size(), 0);
|
||||
int cursor = 0;
|
||||
for (size_t i = 0; i < pipeline.size(); ++i) {
|
||||
const DagNodeDef* def = dag_find(pipeline[i].name);
|
||||
int pc = def ? static_cast<int>(def->param_defaults.size()) : 0;
|
||||
base[i] = cursor;
|
||||
cursor += dag_vec4_count(pc);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
|
||||
const int n = static_cast<int>(std::min(pipeline.size(), static_cast<size_t>(MAX_NODES)));
|
||||
std::ostringstream out;
|
||||
|
||||
out << "uniform vec4 u_params[16];\n";
|
||||
out << "uniform vec4 u_params[" << MAX_PARAM_VEC4S << "];\n";
|
||||
out << "uniform int u_preview_target; // -1 = real Output; >=0 = show out_<i>\n\n";
|
||||
|
||||
if (n == 0) {
|
||||
@@ -23,6 +37,8 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
|
||||
return out.str();
|
||||
}
|
||||
|
||||
std::vector<int> base = dag_param_layout(pipeline);
|
||||
|
||||
// Emit per-node functions (skip Output: it's a sink, no body)
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const DagStep& step = pipeline[static_cast<size_t>(i)];
|
||||
@@ -38,7 +54,7 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
|
||||
if (ni >= 4) out << ", vec4 d";
|
||||
if (ni > 0) out << ", ";
|
||||
out << "vec2 uv) {\n";
|
||||
out << def->body_glsl(i) << "\n";
|
||||
out << def->body_glsl(base[static_cast<size_t>(i)]) << "\n";
|
||||
out << "}\n\n";
|
||||
}
|
||||
|
||||
@@ -67,8 +83,9 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (last_valid_out < 0) return "vec4(0.0, 0.0, 0.0, 1.0)";
|
||||
return "out_" + std::to_string(last_valid_out);
|
||||
// Op/Blend with no source on this slot → black input (cannot fall back to
|
||||
// last_valid_out: that's how nodes "leak" into the canvas without being wired).
|
||||
return "vec4(0.0, 0.0, 0.0, 1.0)";
|
||||
};
|
||||
|
||||
out << " vec4 out_" << i << " = node_" << i << "(";
|
||||
@@ -81,6 +98,7 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
|
||||
|
||||
last_valid_out = i;
|
||||
}
|
||||
(void)last_valid_out;
|
||||
|
||||
// Preview branch: if u_preview_target points to a valid out_<i>, emit it
|
||||
// and bail out before the Output-driven fragColor.
|
||||
@@ -95,6 +113,9 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
|
||||
// Resolve fragColor: if there's an Output node with a connection, use that; else fallback.
|
||||
auto seed = [&]() { out << " fragColor = vec4(0.04, 0.04, 0.06, 1.0);\n"; };
|
||||
|
||||
// Strict policy: only emit what is wired into the Output node. With no
|
||||
// Output present, or with Output left disconnected, paint the seed color —
|
||||
// never silently fall back to the last evaluated node.
|
||||
if (output_idx >= 0) {
|
||||
const std::string& sid = pipeline[static_cast<size_t>(output_idx)].source_ids[0];
|
||||
int src = -1;
|
||||
@@ -104,9 +125,7 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
|
||||
}
|
||||
}
|
||||
if (src >= 0) out << " fragColor = out_" << src << ";\n";
|
||||
else seed();
|
||||
} else if (last_valid_out >= 0) {
|
||||
out << " fragColor = out_" << last_valid_out << ";\n";
|
||||
else seed();
|
||||
} else {
|
||||
seed();
|
||||
}
|
||||
@@ -116,6 +135,56 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
|
||||
return out.str();
|
||||
}
|
||||
|
||||
std::string compile_dag_to_glsl_baked(const std::vector<DagStep>& pipeline) {
|
||||
std::string s = compile_dag_to_glsl(pipeline);
|
||||
|
||||
// Compute total vec4 slots actually used by the pipeline.
|
||||
auto base = dag_param_layout(pipeline);
|
||||
int total = 0;
|
||||
for (size_t i = 0; i < pipeline.size(); ++i) {
|
||||
const DagNodeDef* def = dag_find(pipeline[i].name);
|
||||
int pc = def ? static_cast<int>(def->param_defaults.size()) : 0;
|
||||
int v = dag_vec4_count(pc);
|
||||
if (base[i] + v > total) total = base[i] + v;
|
||||
}
|
||||
if (total == 0) total = 1; // GLSL forbids zero-sized arrays
|
||||
|
||||
// Pack current params into a flat float array (same layout as dag_uniforms_apply).
|
||||
std::vector<float> data(static_cast<size_t>(total * 4), 0.0f);
|
||||
for (size_t i = 0; i < pipeline.size(); ++i) {
|
||||
const DagNodeDef* def = dag_find(pipeline[i].name);
|
||||
if (!def) continue;
|
||||
int pc = static_cast<int>(def->param_defaults.size());
|
||||
int b = base[i] * 4;
|
||||
for (int k = 0; k < pc && k < static_cast<int>(pipeline[i].params.size()); ++k) {
|
||||
data[static_cast<size_t>(b + k)] = pipeline[i].params[static_cast<size_t>(k)];
|
||||
}
|
||||
}
|
||||
|
||||
// Build `const vec4 u_params[N] = vec4[N](vec4(...), ...);`
|
||||
std::ostringstream init;
|
||||
init << "const vec4 u_params[" << total << "] = vec4[" << total << "](";
|
||||
for (int i = 0; i < total; ++i) {
|
||||
if (i > 0) init << ", ";
|
||||
init << "vec4("
|
||||
<< data[static_cast<size_t>(i * 4 + 0)] << ", "
|
||||
<< data[static_cast<size_t>(i * 4 + 1)] << ", "
|
||||
<< data[static_cast<size_t>(i * 4 + 2)] << ", "
|
||||
<< data[static_cast<size_t>(i * 4 + 3)] << ")";
|
||||
}
|
||||
init << ");";
|
||||
|
||||
// Replace the uniform u_params declaration with the const array.
|
||||
static const std::regex up_re(R"(uniform\s+vec4\s+u_params\[\d+\];)");
|
||||
s = std::regex_replace(s, up_re, init.str());
|
||||
|
||||
// Replace the u_preview_target uniform with a const = -1 (kills the preview branches).
|
||||
static const std::regex pt_re(R"(uniform\s+int\s+u_preview_target;[^\n]*)");
|
||||
s = std::regex_replace(s, pt_re, "const int u_preview_target = -1;");
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace fn::gfx
|
||||
|
||||
#ifdef DAG_COMPILE_TEST
|
||||
@@ -136,22 +205,23 @@ int main() {
|
||||
assert(contains(s, "fragColor = vec4(0.04"));
|
||||
}
|
||||
|
||||
// 2. Single Gen → fragColor = out_0
|
||||
// 2. Single Gen + Output wired → fragColor = out_0
|
||||
{
|
||||
std::vector<DagStep> p;
|
||||
DagStep g; g.id = "a"; g.name = "plasma";
|
||||
p.push_back(g);
|
||||
DagStep g; g.id = "a"; g.name = "plasma"; p.push_back(g);
|
||||
DagStep o; o.id = "out"; o.name = "output"; o.source_ids[0] = "a"; p.push_back(o);
|
||||
auto s = compile_dag_to_glsl(p);
|
||||
assert(contains(s, "vec4 node_0"));
|
||||
assert(contains(s, "vec4 out_0 = node_0("));
|
||||
assert(contains(s, "fragColor = out_0"));
|
||||
}
|
||||
|
||||
// 3. Gen + Op → Op uses out_0 as input a
|
||||
// 3. Gen + Op + Output → Op uses out_0 as input, fragColor = out_1
|
||||
{
|
||||
std::vector<DagStep> p;
|
||||
DagStep g; g.id = "a"; g.name = "plasma"; p.push_back(g);
|
||||
DagStep o; o.id = "b"; o.name = "invert"; o.source_ids[0] = "a"; p.push_back(o);
|
||||
DagStep f; f.id = "out"; f.name = "output"; f.source_ids[0] = "b"; p.push_back(f);
|
||||
auto s = compile_dag_to_glsl(p);
|
||||
assert(contains(s, "out_1 = node_1(out_0, uv)"));
|
||||
assert(contains(s, "fragColor = out_1"));
|
||||
@@ -165,8 +235,26 @@ int main() {
|
||||
DagStep m; m.id = "m"; m.name = "blend_mix";
|
||||
m.source_ids[0] = "a"; m.source_ids[1] = "b";
|
||||
p.push_back(m);
|
||||
DagStep o; o.id = "out"; o.name = "output"; o.source_ids[0] = "m"; p.push_back(o);
|
||||
auto s = compile_dag_to_glsl(p);
|
||||
assert(contains(s, "out_2 = node_2(out_0, out_1, uv)"));
|
||||
assert(contains(s, "fragColor = out_2"));
|
||||
}
|
||||
|
||||
// 4b. Strict mode: nodes without Output → seed (never leaks last node).
|
||||
// Note: the preview branch emits `if (u_preview_target == i) fragColor = out_i;`
|
||||
// which we don't penalise; what matters is the *final* fragColor (after the
|
||||
// preview ifs) — that must be the seed, not a node output.
|
||||
{
|
||||
std::vector<DagStep> p;
|
||||
DagStep g; g.id = "a"; g.name = "plasma"; p.push_back(g);
|
||||
auto s = compile_dag_to_glsl(p);
|
||||
assert(contains(s, "vec4 out_0 = node_0(")); // node still emitted
|
||||
// The seed line must appear *after* the last preview branch
|
||||
size_t seed_pos = s.rfind("fragColor = vec4(0.04");
|
||||
size_t preview_pos = s.rfind("u_preview_target ==");
|
||||
assert(seed_pos != std::string::npos);
|
||||
assert(preview_pos == std::string::npos || seed_pos > preview_pos);
|
||||
}
|
||||
|
||||
// 5. Output node drives fragColor from its source, not from last index
|
||||
@@ -193,7 +281,21 @@ int main() {
|
||||
assert(contains(s, "fragColor = vec4(0.04"));
|
||||
}
|
||||
|
||||
std::printf("dag_compile: 6/6 asserts passed\n");
|
||||
// 7. Baked variant: const arrays, no uniforms u_params / u_preview_target
|
||||
{
|
||||
std::vector<DagStep> p;
|
||||
DagStep g; g.id = "a"; g.name = "plasma"; g.params = {2.0f, 3.0f}; p.push_back(g);
|
||||
DagStep o; o.id = "out"; o.name = "output"; o.source_ids[0] = "a"; p.push_back(o);
|
||||
auto s = compile_dag_to_glsl_baked(p);
|
||||
assert(!contains(s, "uniform vec4 u_params"));
|
||||
assert(!contains(s, "uniform int u_preview_target"));
|
||||
assert(contains(s, "const vec4 u_params["));
|
||||
assert(contains(s, "vec4(2")); // baked first param
|
||||
assert(contains(s, "const int u_preview_target = -1"));
|
||||
assert(contains(s, "fragColor = out_0"));
|
||||
}
|
||||
|
||||
std::printf("dag_compile: 8/8 asserts passed\n");
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -7,8 +7,21 @@ namespace fn::gfx {
|
||||
|
||||
// Compila un pipeline DAG a GLSL 330 core completo (listo para gl_shader::compile_fragment).
|
||||
// El preamble de gl_shader ya declara #version, fragColor, u_time, u_resolution, u_mouse.
|
||||
// Este compilador emite uniform vec4 u_params[16], las funciones node_<i> y void main().
|
||||
// Este compilador emite uniform vec4 u_params[64], las funciones node_<i> y void main().
|
||||
// Si el pipeline esta vacio, emite un fragment que pinta gris oscuro.
|
||||
std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline);
|
||||
|
||||
// Devuelve el indice base (vec4) en u_params[] que ocupa cada nodo del pipeline.
|
||||
// Cada nodo ocupa ceil(param_count / 4) vec4s consecutivos. Nodos con 0 params ocupan 0.
|
||||
// El compilador y dag_uniforms_apply usan el mismo layout.
|
||||
std::vector<int> dag_param_layout(const std::vector<DagStep>& pipeline);
|
||||
|
||||
// Variante de compile_dag_to_glsl que sustituye `uniform vec4 u_params[64]`
|
||||
// por un `const vec4 u_params[N] = vec4[N](...)` con los valores actuales del
|
||||
// pipeline empaquetados, y `uniform int u_preview_target` por
|
||||
// `const int u_preview_target = -1`. El resultado es un fragment shader
|
||||
// autocontenido: no depende de ningun uniform externo y se puede pegar tal cual
|
||||
// en el editor Code para reproducir el DAG actual.
|
||||
std::string compile_dag_to_glsl_baked(const std::vector<DagStep>& pipeline);
|
||||
|
||||
} // namespace fn::gfx
|
||||
|
||||
@@ -49,3 +49,16 @@ void main() {
|
||||
- Si el pipeline esta vacio, emite void main() que pinta gris oscuro (0.04, 0.04, 0.06).
|
||||
- MAX_NODES = 16. Pipelines mas largos se truncan silenciosamente.
|
||||
- source_id fallback: si el id no se encuentra o apunta a un indice >= idx, usa max(0, idx-2).
|
||||
|
||||
## Cambios 2026-04-25 (Fase 5 + Fase 7 shaders_lab)
|
||||
|
||||
- **Layout de params dinámico**: el array global pasa de `vec4 u_params[16]` (1 vec4 por nodo) a `vec4 u_params[64]` (`MAX_PARAM_VEC4S`). Cada nodo ocupa `dag_vec4_count(param_count)` vec4s consecutivos. Helper público `dag_param_layout(pipeline) -> vector<int>` devuelve el índice base por nodo y se comparte con `dag_uniforms_apply`.
|
||||
- **Strict output**: el fallback `last_valid_out` que filtraba el output del último nodo cuando `Output` no tenía source o no existía está eliminado. Ahora la regla es: solo se emite lo conectado al `Output`; en cualquier otro caso `seed()` (gris oscuro). El `resolve()` interno también devuelve `vec4(0,0,0,1)` para slots de input vacíos (antes caía a `last_valid_out`).
|
||||
- **Test 4b nuevo**: nodo sin Output → seed final aparece después de las branches de preview (`fragColor = vec4(0.04` después del último `if (u_preview_target ==`).
|
||||
- **Variante baked: `compile_dag_to_glsl_baked(pipeline)`** (nuevo en `.h` + `.cpp`):
|
||||
- Sustituye `uniform vec4 u_params[64];` por `const vec4 u_params[N] = vec4[N](vec4(...), ...);` con los valores actuales del pipeline empaquetados (mismo layout que `dag_uniforms_apply`).
|
||||
- Sustituye `uniform int u_preview_target;` por `const int u_preview_target = -1;`. Las branches de preview quedan muertas.
|
||||
- Sustitución vía `std::regex_replace`. `total = max(base[i] + dag_vec4_count(pc))` o 1 (GLSL prohíbe arrays de tamaño 0).
|
||||
- Caso de uso: panel `Generated GLSL` de shaders_lab muestra el baked, paste-able en el editor `Code` para reproducir el render del DAG sin uniforms externos. Test 7 verifica ausencia de `uniform vec4 u_params` y presencia de `const vec4 u_params[`.
|
||||
|
||||
Cobertura tests: 7/7 (strict + 4b) → **8/8** (incluye baked).
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "imgui.h"
|
||||
#include "imgui_node_editor.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
#include <queue>
|
||||
@@ -21,6 +22,10 @@ static constexpr int MAX_NODES = 16;
|
||||
static ed::EditorContext* s_ctx = nullptr;
|
||||
static uint32_t s_next_uid = 1;
|
||||
static std::unordered_set<uint32_t> s_positioned;
|
||||
// Real pin positions in canvas space, captured during node draw and consulted
|
||||
// by the splice hit-test. Without this, a cable's hit zone is offset from the
|
||||
// visible cable whenever node height ≠ pin row height (e.g. preview open).
|
||||
static std::unordered_map<uintptr_t, ImVec2> s_pin_canvas_pos;
|
||||
|
||||
// ── ID encoding ──────────────────────────────────────────────────────────────
|
||||
// node id = editor_uid
|
||||
@@ -48,6 +53,45 @@ static bool is_output_pin(uintptr_t id) { return (id & 0xFF) == 0; }
|
||||
static uint32_t uid_from_pin(uintptr_t id) { return static_cast<uint32_t>(id >> 8); }
|
||||
static int slot_from_input_pin(uintptr_t id) { return static_cast<int>(id & 0xFF) - 1; }
|
||||
|
||||
// Closest distance from point p to the segment [a, b] (canvas space).
|
||||
static float dist_point_to_segment(ImVec2 p, ImVec2 a, ImVec2 b) {
|
||||
float abx = b.x - a.x, aby = b.y - a.y;
|
||||
float apx = p.x - a.x, apy = p.y - a.y;
|
||||
float ab2 = abx * abx + aby * aby;
|
||||
if (ab2 <= 1e-6f) return std::sqrt(apx * apx + apy * apy);
|
||||
float t = (apx * abx + apy * aby) / ab2;
|
||||
t = std::max(0.0f, std::min(1.0f, t));
|
||||
float qx = a.x + t * abx, qy = a.y + t * aby;
|
||||
float dx = p.x - qx, dy = p.y - qy;
|
||||
return std::sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
// Same horizontal-bias control offset imgui-node-editor uses for its links.
|
||||
static inline float bezier_ctrl(float dx) {
|
||||
return std::max(40.0f, std::abs(dx) * 0.5f);
|
||||
}
|
||||
|
||||
// Closest distance from point p to a cubic bezier curve, sampled as 24 chords.
|
||||
static float dist_point_to_bezier(ImVec2 p, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3) {
|
||||
constexpr int N = 24;
|
||||
float best = 1e30f;
|
||||
ImVec2 prev = p0;
|
||||
for (int i = 1; i <= N; ++i) {
|
||||
float t = static_cast<float>(i) / static_cast<float>(N);
|
||||
float u = 1.0f - t;
|
||||
float b0 = u * u * u;
|
||||
float b1 = 3.0f * u * u * t;
|
||||
float b2 = 3.0f * u * t * t;
|
||||
float b3 = t * t * t;
|
||||
ImVec2 pt(b0 * p0.x + b1 * p1.x + b2 * p2.x + b3 * p3.x,
|
||||
b0 * p0.y + b1 * p1.y + b2 * p2.y + b3 * p3.y);
|
||||
float d = dist_point_to_segment(p, prev, pt);
|
||||
if (d < best) best = d;
|
||||
prev = pt;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
static int find_by_uid(const std::vector<DagStep>& p, uint32_t uid) {
|
||||
for (int i = 0; i < static_cast<int>(p.size()); ++i) {
|
||||
if (p[static_cast<size_t>(i)].editor_uid == uid) return i;
|
||||
@@ -71,10 +115,14 @@ static ImVec4 kind_color(DagKind kind) {
|
||||
return ImVec4(1, 1, 1, 1);
|
||||
}
|
||||
|
||||
static constexpr float PIN_RADIUS = 9.0f;
|
||||
static constexpr float PIN_DIAMETER = PIN_RADIUS * 2.0f;
|
||||
static const ImVec4 PIN_COLOR = ImVec4(0.78f, 0.78f, 0.82f, 1.0f);
|
||||
static const ImVec4 PIN_BORDER = ImVec4(0.20f, 0.20f, 0.22f, 1.0f);
|
||||
static constexpr float PIN_RADIUS = 14.0f; // big grabbable target
|
||||
static constexpr float PIN_DIAMETER = PIN_RADIUS * 2.0f;
|
||||
static constexpr float CONTROL_WIDTH = 220.0f;
|
||||
static constexpr float COL_GAP = 14.0f; // input ↔ controls ↔ output gap
|
||||
static constexpr float CABLE_THICK = 3.5f;
|
||||
static const ImVec4 PIN_COLOR = ImVec4(0.78f, 0.78f, 0.82f, 1.0f);
|
||||
static const ImVec4 PIN_BORDER = ImVec4(0.20f, 0.20f, 0.22f, 1.0f);
|
||||
static const ImVec4 SPLICE_COLOR = ImVec4(1.00f, 0.82f, 0.18f, 1.0f); // golden preview cable
|
||||
|
||||
enum class PinSide { Input, Output };
|
||||
|
||||
@@ -90,7 +138,7 @@ static void draw_pin_circle(PinSide side) {
|
||||
ImVec2(center.x + PIN_RADIUS, center.y + PIN_RADIUS));
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddCircleFilled(center, PIN_RADIUS, ImGui::ColorConvertFloat4ToU32(PIN_COLOR));
|
||||
dl->AddCircle(center, PIN_RADIUS, ImGui::ColorConvertFloat4ToU32(PIN_BORDER), 0, 1.5f);
|
||||
dl->AddCircle(center, PIN_RADIUS, ImGui::ColorConvertFloat4ToU32(PIN_BORDER), 0, 2.0f);
|
||||
ImGui::Dummy(ImVec2(PIN_RADIUS, PIN_DIAMETER));
|
||||
}
|
||||
|
||||
@@ -197,30 +245,238 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
|
||||
|
||||
if (s_pending_add) {
|
||||
const DagNodeDef* def = dag_find(s_pending_add_name);
|
||||
if (def && static_cast<int>(pipeline.size()) < MAX_NODES) {
|
||||
uint32_t uid = s_next_uid++;
|
||||
DagStep step;
|
||||
step.id = "n" + std::to_string(uid);
|
||||
step.name = def->name;
|
||||
step.params = def->param_defaults;
|
||||
step.editor_uid = uid;
|
||||
ImVec2 canvas_pos = ed::ScreenToCanvas(s_pending_add_pos);
|
||||
step.editor_pos_x = canvas_pos.x;
|
||||
step.editor_pos_y = canvas_pos.y;
|
||||
// Insert before the Output node so the Output stays at the back;
|
||||
// otherwise new nodes can never be wired into it (compiler and
|
||||
// cycle check only search indices strictly before the target).
|
||||
auto insert_it = pipeline.end();
|
||||
for (auto it = pipeline.begin(); it != pipeline.end(); ++it) {
|
||||
const DagNodeDef* d = dag_find(it->name);
|
||||
if (d && d->kind == DagKind::Output) { insert_it = it; break; }
|
||||
if (def) {
|
||||
ImVec2 drop = ed::ScreenToCanvas(s_pending_add_pos);
|
||||
|
||||
// ── Priority 1: drop on an existing cable → splice (src → new → dst).
|
||||
// Only valid if the new def actually has an input pin (Op / Blend).
|
||||
int splice_src_idx = -1, splice_dst_idx = -1, splice_slot = -1;
|
||||
if (def->num_inputs >= 1 && def->kind != DagKind::Output) {
|
||||
constexpr float HIT_THRESH = 14.0f; // canvas px
|
||||
float best_d = HIT_THRESH;
|
||||
for (size_t i = 0; i < pipeline.size(); ++i) {
|
||||
const DagStep& dst = pipeline[i];
|
||||
const DagNodeDef* dd = dag_find(dst.name);
|
||||
if (!dd) continue;
|
||||
for (int k = 0; k < dd->num_inputs; ++k) {
|
||||
const std::string& sid = dst.source_ids[static_cast<size_t>(k)];
|
||||
if (sid.empty()) continue;
|
||||
int src_idx = find_by_id(pipeline, sid);
|
||||
if (src_idx < 0) continue;
|
||||
const DagStep& src = pipeline[static_cast<size_t>(src_idx)];
|
||||
|
||||
auto out_it = s_pin_canvas_pos.find(output_pin_id(src.editor_uid));
|
||||
auto in_it = s_pin_canvas_pos.find(input_pin_id(dst.editor_uid, k));
|
||||
if (out_it == s_pin_canvas_pos.end() ||
|
||||
in_it == s_pin_canvas_pos.end()) continue;
|
||||
ImVec2 A = out_it->second;
|
||||
ImVec2 B = in_it->second;
|
||||
float ctrl = bezier_ctrl(B.x - A.x);
|
||||
ImVec2 P1(A.x + ctrl, A.y);
|
||||
ImVec2 P2(B.x - ctrl, B.y);
|
||||
float d = dist_point_to_bezier(drop, A, P1, P2, B);
|
||||
if (d < best_d) {
|
||||
best_d = d;
|
||||
splice_src_idx = src_idx;
|
||||
splice_dst_idx = static_cast<int>(i);
|
||||
splice_slot = k;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Priority 2: drop on an existing node of the same kind → replace.
|
||||
int hit_idx = -1;
|
||||
if (splice_dst_idx < 0) {
|
||||
for (size_t i = 0; i < pipeline.size(); ++i) {
|
||||
auto npos = ed::GetNodePosition(ed::NodeId(node_id(pipeline[i].editor_uid)));
|
||||
auto nsz = ed::GetNodeSize (ed::NodeId(node_id(pipeline[i].editor_uid)));
|
||||
if (drop.x >= npos.x && drop.x <= npos.x + nsz.x &&
|
||||
drop.y >= npos.y && drop.y <= npos.y + nsz.y) {
|
||||
hit_idx = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const DagNodeDef* hit_def = (hit_idx >= 0)
|
||||
? dag_find(pipeline[static_cast<size_t>(hit_idx)].name) : nullptr;
|
||||
|
||||
if (splice_dst_idx >= 0 && static_cast<int>(pipeline.size()) < MAX_NODES) {
|
||||
// Splice: build the new node wired to the existing source, then
|
||||
// rewire the existing destination's input to point to it.
|
||||
const std::string src_id = pipeline[static_cast<size_t>(splice_src_idx)].id;
|
||||
const std::string dst_id = pipeline[static_cast<size_t>(splice_dst_idx)].id;
|
||||
|
||||
uint32_t uid = s_next_uid++;
|
||||
DagStep step;
|
||||
step.id = "n" + std::to_string(uid);
|
||||
step.name = def->name;
|
||||
step.params = def->param_defaults;
|
||||
step.editor_uid = uid;
|
||||
step.editor_pos_x = drop.x;
|
||||
step.editor_pos_y = drop.y;
|
||||
step.source_ids[0] = src_id; // wire src → new
|
||||
|
||||
auto insert_it = pipeline.end();
|
||||
for (auto it = pipeline.begin(); it != pipeline.end(); ++it) {
|
||||
const DagNodeDef* d = dag_find(it->name);
|
||||
if (d && d->kind == DagKind::Output) { insert_it = it; break; }
|
||||
}
|
||||
pipeline.insert(insert_it, step);
|
||||
|
||||
// Re-find dst by id (insertion may have shifted indices) and
|
||||
// rewire its slot to the new node.
|
||||
int dst_now = find_by_id(pipeline, dst_id);
|
||||
if (dst_now >= 0) {
|
||||
pipeline[static_cast<size_t>(dst_now)].source_ids[static_cast<size_t>(splice_slot)] = step.id;
|
||||
}
|
||||
changed = true;
|
||||
} else if (hit_def && hit_def->kind == def->kind && def->kind != DagKind::Output) {
|
||||
// Replace path: same-kind node hit. Keep id, editor_uid, pos,
|
||||
// source_ids, preview_open. Reset params + clear stale input
|
||||
// slots beyond the new def's input count.
|
||||
DagStep& tgt = pipeline[static_cast<size_t>(hit_idx)];
|
||||
tgt.name = def->name;
|
||||
tgt.params = def->param_defaults;
|
||||
for (int k = def->num_inputs; k < 4; ++k) {
|
||||
tgt.source_ids[static_cast<size_t>(k)].clear();
|
||||
}
|
||||
changed = true;
|
||||
} else if (static_cast<int>(pipeline.size()) < MAX_NODES) {
|
||||
// Add path: brand-new node, inserted before Output so the sink stays last.
|
||||
uint32_t uid = s_next_uid++;
|
||||
DagStep step;
|
||||
step.id = "n" + std::to_string(uid);
|
||||
step.name = def->name;
|
||||
step.params = def->param_defaults;
|
||||
step.editor_uid = uid;
|
||||
step.editor_pos_x = drop.x;
|
||||
step.editor_pos_y = drop.y;
|
||||
auto insert_it = pipeline.end();
|
||||
for (auto it = pipeline.begin(); it != pipeline.end(); ++it) {
|
||||
const DagNodeDef* d = dag_find(it->name);
|
||||
if (d && d->kind == DagKind::Output) { insert_it = it; break; }
|
||||
}
|
||||
pipeline.insert(insert_it, step);
|
||||
changed = true;
|
||||
}
|
||||
pipeline.insert(insert_it, step);
|
||||
changed = true;
|
||||
}
|
||||
s_pending_add = false;
|
||||
}
|
||||
|
||||
// ── Live splice candidate detection ─────────────────────────────────────
|
||||
// The user can splice into a cable in two ways:
|
||||
// (a) drag a node from the palette → ImGui drag-drop payload.
|
||||
// (b) drag an existing node by its body → tracked via mouse-down on a node.
|
||||
// In both cases, while the drag is active we hit-test against existing
|
||||
// cables and remember the candidate so:
|
||||
// 1. the link-drawing pass below paints it in SPLICE_COLOR (preview).
|
||||
// 2. the release handler farther down rewires the graph.
|
||||
static uint32_t s_drag_existing_uid = 0;
|
||||
|
||||
// Start tracking an existing-node drag when the user mouse-down on a node
|
||||
// body (not on a pin, not on the Output sink, must have at least one input).
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
ed::PinId hp = ed::GetHoveredPin();
|
||||
ed::NodeId hn = ed::GetHoveredNode();
|
||||
if (hp.Get() == 0 && hn.Get() != 0) {
|
||||
uint32_t uid = static_cast<uint32_t>(hn.Get());
|
||||
int idx = find_by_uid(pipeline, uid);
|
||||
if (idx >= 0) {
|
||||
const DagNodeDef* d = dag_find(pipeline[static_cast<size_t>(idx)].name);
|
||||
if (d && d->num_inputs >= 1 && d->kind != DagKind::Output) {
|
||||
s_drag_existing_uid = uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve which definition (if any) is the active splice candidate.
|
||||
const DagNodeDef* candidate_def = nullptr;
|
||||
const std::string* exclude_node_id = nullptr;
|
||||
int exclude_node_idx = -1;
|
||||
if (const ImGuiPayload* p = ImGui::GetDragDropPayload()) {
|
||||
if (p->IsDataType("DAG_NODE_TYPE")) {
|
||||
std::string drag_name(static_cast<const char*>(p->Data),
|
||||
static_cast<size_t>(p->DataSize));
|
||||
candidate_def = dag_find(drag_name);
|
||||
}
|
||||
}
|
||||
if (!candidate_def && s_drag_existing_uid != 0) {
|
||||
exclude_node_idx = find_by_uid(pipeline, s_drag_existing_uid);
|
||||
if (exclude_node_idx >= 0) {
|
||||
candidate_def = dag_find(pipeline[static_cast<size_t>(exclude_node_idx)].name);
|
||||
exclude_node_id = &pipeline[static_cast<size_t>(exclude_node_idx)].id;
|
||||
}
|
||||
}
|
||||
|
||||
// Hit-test cables against current mouse position (canvas space).
|
||||
uint32_t splice_hl_from_uid = 0;
|
||||
uint32_t splice_hl_to_uid = 0;
|
||||
int splice_hl_slot = -1;
|
||||
if (candidate_def && candidate_def->num_inputs >= 1
|
||||
&& candidate_def->kind != DagKind::Output) {
|
||||
ImVec2 cur = ed::ScreenToCanvas(ImGui::GetMousePos());
|
||||
constexpr float HIT_THRESH = 16.0f;
|
||||
float best_d = HIT_THRESH;
|
||||
for (size_t i = 0; i < pipeline.size(); ++i) {
|
||||
const DagStep& dst = pipeline[i];
|
||||
// Skip cables that touch the moving node (its own in/out edges).
|
||||
if (exclude_node_id && dst.id == *exclude_node_id) continue;
|
||||
const DagNodeDef* dd = dag_find(dst.name);
|
||||
if (!dd) continue;
|
||||
for (int k = 0; k < dd->num_inputs; ++k) {
|
||||
const std::string& sid = dst.source_ids[static_cast<size_t>(k)];
|
||||
if (sid.empty()) continue;
|
||||
if (exclude_node_id && sid == *exclude_node_id) continue;
|
||||
int src_idx = find_by_id(pipeline, sid);
|
||||
if (src_idx < 0) continue;
|
||||
const DagStep& src = pipeline[static_cast<size_t>(src_idx)];
|
||||
auto out_it = s_pin_canvas_pos.find(output_pin_id(src.editor_uid));
|
||||
auto in_it = s_pin_canvas_pos.find(input_pin_id(dst.editor_uid, k));
|
||||
if (out_it == s_pin_canvas_pos.end() ||
|
||||
in_it == s_pin_canvas_pos.end()) continue;
|
||||
ImVec2 A = out_it->second;
|
||||
ImVec2 B = in_it->second;
|
||||
float ctrl = bezier_ctrl(B.x - A.x);
|
||||
ImVec2 P1(A.x + ctrl, A.y);
|
||||
ImVec2 P2(B.x - ctrl, B.y);
|
||||
float d = dist_point_to_bezier(cur, A, P1, P2, B);
|
||||
if (d < best_d) {
|
||||
best_d = d;
|
||||
splice_hl_from_uid = src.editor_uid;
|
||||
splice_hl_to_uid = dst.editor_uid;
|
||||
splice_hl_slot = k;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release handler for the existing-node-drag splice. Palette splice goes
|
||||
// through s_pending_add above; this branch handles "drag node body onto cable".
|
||||
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) && s_drag_existing_uid != 0) {
|
||||
uint32_t moving_uid = s_drag_existing_uid;
|
||||
s_drag_existing_uid = 0;
|
||||
if (splice_hl_to_uid != 0) {
|
||||
int mv_idx = find_by_uid(pipeline, moving_uid);
|
||||
int src_idx = find_by_uid(pipeline, splice_hl_from_uid);
|
||||
int dst_idx = find_by_uid(pipeline, splice_hl_to_uid);
|
||||
if (mv_idx >= 0 && src_idx >= 0 && dst_idx >= 0) {
|
||||
const std::string moving_id = pipeline[static_cast<size_t>(mv_idx)].id;
|
||||
const std::string src_id = pipeline[static_cast<size_t>(src_idx)].id;
|
||||
// Detach moving node from any existing consumer.
|
||||
for (auto& s : pipeline) {
|
||||
for (auto& sid : s.source_ids) {
|
||||
if (sid == moving_id) sid.clear();
|
||||
}
|
||||
}
|
||||
pipeline[static_cast<size_t>(mv_idx)].source_ids[0] = src_id;
|
||||
pipeline[static_cast<size_t>(dst_idx)].source_ids[static_cast<size_t>(splice_hl_slot)] = moving_id;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Draw nodes ───────────────────────────────────────────────────────────
|
||||
for (int i = 0; i < static_cast<int>(pipeline.size()); ++i) {
|
||||
DagStep& step = pipeline[static_cast<size_t>(i)];
|
||||
@@ -233,7 +489,7 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
|
||||
// and user drags must not be overwritten.
|
||||
if (s_positioned.find(step.editor_uid) == s_positioned.end()) {
|
||||
if (step.editor_pos_x == 0.0f && step.editor_pos_y == 0.0f) {
|
||||
step.editor_pos_x = 50.0f + static_cast<float>(i) * 220.0f;
|
||||
step.editor_pos_x = 50.0f + static_cast<float>(i) * 320.0f;
|
||||
step.editor_pos_y = 100.0f;
|
||||
}
|
||||
ed::SetNodePosition(ed::NodeId(node_id(step.editor_uid)),
|
||||
@@ -243,7 +499,7 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
|
||||
|
||||
// Zero lateral padding so the input/output pin circles sit flush
|
||||
// with the node's left and right edges.
|
||||
ed::PushStyleVar(ed::StyleVar_NodePadding, ImVec4(0, 8, 0, 8));
|
||||
ed::PushStyleVar(ed::StyleVar_NodePadding, ImVec4(0, 12, 0, 12));
|
||||
ed::BeginNode(ed::NodeId(node_id(step.editor_uid)));
|
||||
|
||||
// Header (with horizontal padding so the title doesn't touch the edge)
|
||||
@@ -266,36 +522,43 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
|
||||
ed::BeginPin(ed::PinId(input_pin_id(step.editor_uid, k)), ed::PinKind::Input);
|
||||
ed::PinPivotAlignment(ImVec2(0.0f, 0.5f));
|
||||
ed::PinPivotSize(ImVec2(0, 0));
|
||||
ImVec2 cur_screen = ImGui::GetCursorScreenPos();
|
||||
ImVec2 center_screen(cur_screen.x, cur_screen.y + PIN_RADIUS);
|
||||
s_pin_canvas_pos[input_pin_id(step.editor_uid, k)] =
|
||||
ed::ScreenToCanvas(center_screen);
|
||||
draw_pin_circle(PinSide::Input);
|
||||
ed::EndPin();
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
|
||||
ImGui::SameLine(0, 8); // gap between pin column and controls
|
||||
ImGui::SameLine(0, COL_GAP); // gap between pin column and controls
|
||||
|
||||
ImGui::BeginGroup(); // controls column (centre, with internal padding)
|
||||
ImGui::PushID(static_cast<int>(step.editor_uid));
|
||||
if (def->controls.empty() && def->kind != DagKind::Output) {
|
||||
ImGui::Dummy(ImVec2(60, PIN_DIAMETER));
|
||||
ImGui::Dummy(ImVec2(CONTROL_WIDTH * 0.5f, PIN_DIAMETER));
|
||||
}
|
||||
for (size_t ci = 0; ci < def->controls.size(); ++ci) {
|
||||
const DagControl& ctrl = def->controls[ci];
|
||||
ImGui::SetNextItemWidth(150.0f);
|
||||
ImGui::SetNextItemWidth(CONTROL_WIDTH);
|
||||
char uid_lbl[64];
|
||||
std::snprintf(uid_lbl, sizeof(uid_lbl), "%s##%u%zu", ctrl.label.c_str(), step.editor_uid, ci);
|
||||
int pcount = static_cast<int>(step.params.size());
|
||||
if (ctrl.kind == DagControl::Kind::Slider) {
|
||||
int pidx = ctrl.param_idx[0];
|
||||
if (pidx >= 0 && pidx < 4) {
|
||||
if (pidx >= 0 && pidx < pcount) {
|
||||
ImGui::SliderFloat(uid_lbl, &step.params[static_cast<size_t>(pidx)], ctrl.min, ctrl.max);
|
||||
}
|
||||
} else if (ctrl.kind == DagControl::Kind::XY) {
|
||||
int px = ctrl.param_idx[0], py = ctrl.param_idx[1];
|
||||
if (px >= 0 && px < 4 && py >= 0 && py < 4 && py == px + 1) {
|
||||
if (px >= 0 && px < pcount && py >= 0 && py < pcount && py == px + 1) {
|
||||
ImGui::SliderFloat2(uid_lbl, &step.params[static_cast<size_t>(px)], ctrl.min, ctrl.max);
|
||||
}
|
||||
} else if (ctrl.kind == DagControl::Kind::Color) {
|
||||
int pr = ctrl.param_idx[0];
|
||||
if (pr >= 0 && pr + 2 < 4) {
|
||||
if (pr >= 0 && pr + 2 < pcount) {
|
||||
ImGui::TextUnformatted(ctrl.label.c_str());
|
||||
ImGui::SameLine();
|
||||
ed::Suspend();
|
||||
ImGui::ColorEdit3(uid_lbl, &step.params[static_cast<size_t>(pr)],
|
||||
ImGuiColorEditFlags_NoInputs |
|
||||
@@ -327,13 +590,17 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
|
||||
ImGui::PopID();
|
||||
ImGui::EndGroup();
|
||||
|
||||
ImGui::SameLine(0, 8); // gap between controls and output pin
|
||||
ImGui::SameLine(0, COL_GAP); // gap between controls and output pin
|
||||
|
||||
ImGui::BeginGroup(); // output column (right edge)
|
||||
if (has_output_pin) {
|
||||
ed::BeginPin(ed::PinId(output_pin_id(step.editor_uid)), ed::PinKind::Output);
|
||||
ed::PinPivotAlignment(ImVec2(1.0f, 0.5f));
|
||||
ed::PinPivotSize(ImVec2(0, 0));
|
||||
ImVec2 cur_screen = ImGui::GetCursorScreenPos();
|
||||
ImVec2 center_screen(cur_screen.x + PIN_RADIUS, cur_screen.y + PIN_RADIUS);
|
||||
s_pin_canvas_pos[output_pin_id(step.editor_uid)] =
|
||||
ed::ScreenToCanvas(center_screen);
|
||||
draw_pin_circle(PinSide::Output);
|
||||
ed::EndPin();
|
||||
} else {
|
||||
@@ -356,10 +623,16 @@ bool dag_node_editor(std::vector<DagStep>& pipeline) {
|
||||
int src_idx = find_by_id(pipeline, sid);
|
||||
if (src_idx < 0) continue;
|
||||
const DagStep& src_step = pipeline[static_cast<size_t>(src_idx)];
|
||||
const bool is_splice_preview =
|
||||
(src_step.editor_uid == splice_hl_from_uid &&
|
||||
step.editor_uid == splice_hl_to_uid &&
|
||||
k == splice_hl_slot);
|
||||
ImVec4 link_col = is_splice_preview ? SPLICE_COLOR : PIN_COLOR;
|
||||
float link_thick = is_splice_preview ? CABLE_THICK + 2.0f : CABLE_THICK;
|
||||
ed::Link(ed::LinkId(link_id(src_step.editor_uid, step.editor_uid, k)),
|
||||
ed::PinId(output_pin_id(src_step.editor_uid)),
|
||||
ed::PinId(input_pin_id(step.editor_uid, k)),
|
||||
PIN_COLOR, 2.5f);
|
||||
link_col, link_thick);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,3 +41,29 @@ Multi-source: cada nodo declara `num_inputs` (0-4). Los slots de `source_ids[]`
|
||||
## Dependencia
|
||||
|
||||
Requiere `imgui_node_editor` static library linkeada (`cpp/vendor/imgui-node-editor`).
|
||||
|
||||
## Notas (2026-04-25, Fase 7 shaders_lab)
|
||||
|
||||
Pulido visual y de UX para conexión:
|
||||
- Pines más grandes para grab fácil: `PIN_RADIUS` 9 → 14, `CABLE_THICK` 2.5 → 3.5, `CONTROL_WIDTH` 150 → 220, `COL_GAP` 8 → 14.
|
||||
- Espaciado inicial entre nodos auto-colocados subido a 320 px.
|
||||
- Bug fix: el control `Color` se renderizaba con `ImGuiColorEditFlags_NoLabel`, así que nodos cuya único control era Color (`solid`) parecían sin nombre. Ahora se imprime `TextUnformatted(label) + SameLine` antes del swatch.
|
||||
|
||||
Drop comportamiento (en orden de prioridad al soltar un nodo de la paleta o arrastrar un nodo del canvas):
|
||||
1. **Drop sobre cable** (`dist_point_to_segment` < 18 px en canvas-space): splice. Solo aplica a nodos con `num_inputs >= 1` y kind != Output.
|
||||
2. **Drop sobre nodo del mismo `DagKind`**: replace. Conserva `id`, `editor_uid`, `editor_pos_x/y`, `source_ids[]`, `preview_open`. Limpia slots de input que sobran si el nuevo def tiene menos `num_inputs`.
|
||||
3. **Drop en vacío**: add. Inserción antes del `Output` para que el sink se quede al final.
|
||||
|
||||
Tracking de drag de nodo existente: `s_drag_existing_uid` se setea en `IsMouseClicked(0)` cuando hay `GetHoveredNode() != 0` y `GetHoveredPin() == 0`. Al soltar, si un cable estaba highlighted, se hace splice (clear de refs hacia el nodo, `mv.source_ids[0] = src.id`, `dst.source_ids[slot] = mv.id`).
|
||||
|
||||
Hit-test contra cajas de nodos vía `ed::GetNodePosition + ed::GetNodeSize` (no se usa `ed::GetHoveredNode` porque no es fiable bajo drag-drop activo).
|
||||
|
||||
Splice highlight (preview live):
|
||||
- Mientras hay payload `DAG_NODE_TYPE` (paleta) o `s_drag_existing_uid` activo, hit-test contra cables (distance point-segment).
|
||||
- El cable candidato se pinta con `SPLICE_COLOR = (1.00, 0.82, 0.18, 1)` y `CABLE_THICK + 2` en `ed::Link()`.
|
||||
- **Garantía visual**: además se dibuja un bezier dorado en `ImGui::GetForegroundDrawList()` con `AddBezierCubic(P0, P1, P2, P3, color, CABLE_THICK + 4)` para no depender del compositing interno de imgui-node-editor.
|
||||
- Sin gates `IsMouseDown` / `window_hovered` (silenciaban el highlight). El payload o `s_drag_existing_uid` ya implican drag activo.
|
||||
|
||||
Constantes públicas vivas (en el .cpp, no exportadas):
|
||||
- `PIN_RADIUS = 14`, `CABLE_THICK = 3.5`, `CONTROL_WIDTH = 220`, `COL_GAP = 14`.
|
||||
- `PIN_COLOR = (0.78, 0.78, 0.82, 1)`, `PIN_BORDER = (0.20, 0.20, 0.22, 1)`, `SPLICE_COLOR = (1.00, 0.82, 0.18, 1)`.
|
||||
|
||||
@@ -173,22 +173,23 @@ bool dag_panel(std::vector<DagStep>& pipeline) {
|
||||
}
|
||||
|
||||
// Controls
|
||||
int pcount = static_cast<int>(step.params.size());
|
||||
for (const auto& ctrl : def->controls) {
|
||||
std::string uid_label = ctrl.label + "##" + step.id + std::to_string(&ctrl - def->controls.data());
|
||||
if (ctrl.kind == DagControl::Kind::Slider) {
|
||||
int pidx = ctrl.param_idx[0];
|
||||
if (pidx >= 0 && pidx < 4) {
|
||||
if (pidx >= 0 && pidx < pcount) {
|
||||
ImGui::SliderFloat(uid_label.c_str(), &step.params[static_cast<size_t>(pidx)], ctrl.min, ctrl.max);
|
||||
}
|
||||
} else if (ctrl.kind == DagControl::Kind::XY) {
|
||||
int px = ctrl.param_idx[0];
|
||||
int py = ctrl.param_idx[1];
|
||||
if (px >= 0 && px < 4 && py >= 0 && py < 4 && py == px + 1) {
|
||||
if (px >= 0 && px < pcount && py >= 0 && py < pcount && py == px + 1) {
|
||||
ImGui::SliderFloat2(uid_label.c_str(), &step.params[static_cast<size_t>(px)], ctrl.min, ctrl.max);
|
||||
}
|
||||
} else if (ctrl.kind == DagControl::Kind::Color) {
|
||||
int pr = ctrl.param_idx[0];
|
||||
if (pr >= 0 && pr + 2 < 4) {
|
||||
if (pr >= 0 && pr + 2 < pcount) {
|
||||
ImGui::ColorEdit3(uid_label.c_str(), &step.params[static_cast<size_t>(pr)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace fn::gfx {
|
||||
|
||||
enum class DagKind { Gen, Op, Blend, Output };
|
||||
|
||||
// Param indices for a control reference floats inside the node's own block
|
||||
// (range 0..param_count-1). Up to 3 indices are used (Color uses 3 contiguous,
|
||||
// XY uses 2 contiguous, Slider uses 1).
|
||||
struct DagControl {
|
||||
enum class Kind { Slider, XY, Color };
|
||||
Kind kind;
|
||||
@@ -24,21 +27,31 @@ struct DagNodeDef {
|
||||
std::string desc;
|
||||
DagKind kind = DagKind::Gen;
|
||||
int num_inputs = 0; // 0=Gen, 1=Op, 2=Blend, up to 4
|
||||
std::array<std::string, 4> param_names{"", "", "", ""};
|
||||
std::array<float, 4> param_defaults{0, 0, 0, 0};
|
||||
std::vector<DagControl> controls;
|
||||
std::function<std::string(int idx)> body_glsl;
|
||||
std::vector<std::string> param_names;
|
||||
std::vector<float> param_defaults;
|
||||
std::vector<DagControl> controls;
|
||||
// body_glsl receives the base vec4 index where this node's params live in
|
||||
// the global u_params[] array (0 if param_count == 0; same value for nodes
|
||||
// that fit in a single vec4).
|
||||
std::function<std::string(int base_vec4)> body_glsl;
|
||||
bool is_builtin = true; // user-saved generators set this false
|
||||
};
|
||||
|
||||
struct DagStep {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::array<float, 4> params{0, 0, 0, 0};
|
||||
std::array<std::string, 4> source_ids{"", "", "", ""}; // up to 4 inputs; "" = no connection
|
||||
std::vector<float> params; // size == def->param_defaults.size()
|
||||
std::array<std::string, 4> source_ids{"", "", "", ""}; // up to 4 inputs; "" = no connection
|
||||
float editor_pos_x = 0.0f;
|
||||
float editor_pos_y = 0.0f;
|
||||
uint32_t editor_uid = 0; // monotonic counter, used as node editor ID
|
||||
bool preview_open = false; // show in-node thumbnail of out_<index>
|
||||
};
|
||||
|
||||
// Number of vec4 slots a node with `param_count` floats occupies. 0 -> 0.
|
||||
inline int dag_vec4_count(int param_count) {
|
||||
if (param_count <= 0) return 0;
|
||||
return (param_count + 3) / 4;
|
||||
}
|
||||
|
||||
} // namespace fn::gfx
|
||||
|
||||
@@ -1,27 +1,38 @@
|
||||
#include "gfx/dag_uniforms.h"
|
||||
#include "gfx/dag_compile.h"
|
||||
#include "gfx/dag_catalog.h"
|
||||
#include "gfx/gl_loader.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace fn::gfx {
|
||||
|
||||
static constexpr int MAX_NODES = 16;
|
||||
static constexpr int MAX_PARAM_VEC4S = 64;
|
||||
|
||||
void dag_uniforms_apply(const std::vector<DagStep>& pipeline, unsigned int program) {
|
||||
float data[MAX_NODES * 4];
|
||||
float data[MAX_PARAM_VEC4S * 4];
|
||||
std::memset(data, 0, sizeof(data));
|
||||
|
||||
const int n = static_cast<int>(std::min(pipeline.size(), static_cast<size_t>(MAX_NODES)));
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const auto& step = pipeline[static_cast<size_t>(i)];
|
||||
data[i * 4 + 0] = step.params[0];
|
||||
data[i * 4 + 1] = step.params[1];
|
||||
data[i * 4 + 2] = step.params[2];
|
||||
data[i * 4 + 3] = step.params[3];
|
||||
std::vector<int> base = dag_param_layout(pipeline);
|
||||
|
||||
for (size_t i = 0; i < pipeline.size(); ++i) {
|
||||
const DagStep& step = pipeline[i];
|
||||
const DagNodeDef* def = dag_find(step.name);
|
||||
if (!def) continue;
|
||||
int pc = static_cast<int>(def->param_defaults.size());
|
||||
int b = base[i] * 4;
|
||||
for (int k = 0; k < pc && b + k < MAX_PARAM_VEC4S * 4; ++k) {
|
||||
data[b + k] = (k < static_cast<int>(step.params.size())) ? step.params[static_cast<size_t>(k)] : 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
GLint loc = glGetUniformLocation(program, "u_params");
|
||||
if (loc >= 0) glUniform4fv(loc, MAX_NODES, data);
|
||||
if (loc >= 0) glUniform4fv(loc, MAX_PARAM_VEC4S, data);
|
||||
|
||||
// Default render path: ensure preview branch in the compiled DAG shader is
|
||||
// disabled (per-node previews override this transiently in dag_previews_render).
|
||||
GLint loc_pt = glGetUniformLocation(program, "u_preview_target");
|
||||
if (loc_pt >= 0) glUniform1i(loc_pt, -1);
|
||||
}
|
||||
|
||||
} // namespace fn::gfx
|
||||
|
||||
@@ -31,3 +31,9 @@ output: "Efecto lateral: actualiza el uniform u_params[16] en el programa GL act
|
||||
## Notas
|
||||
|
||||
El array data[64] se inicializa a 0 antes de copiar, por lo que steps no usados quedan en cero. El caller es responsable de activar el programa antes de llamar.
|
||||
|
||||
## Cambios 2026-04-25 (Fase 5 + Fase 7 shaders_lab)
|
||||
|
||||
- **Layout dinámico**: el array global pasa de `vec4 u_params[16]` (4 floats por nodo, fijo) a `vec4 u_params[64]` (256 floats). Cada nodo ocupa `dag_vec4_count(param_count)` vec4s consecutivos. El packing usa `dag_param_layout(pipeline)` (declarada en `dag_compile.h`) para obtener el índice base por nodo, idéntico al que usa el compilador.
|
||||
- **Reset de `u_preview_target`**: al final del apply, se hace `glUniform1i(u_preview_target, -1)` si el uniform existe en el programa. Esto deja la rama de preview desactivada en el render principal del Canvas DAG; `dag_previews_render` la activa transitoriamente por nodo y la deja restaurada.
|
||||
- Nuevo `dag_compile_cpp_gfx` en `uses_functions` (consume `dag_param_layout`).
|
||||
|
||||
@@ -34,6 +34,17 @@ PFNGLUNIFORM4FVPROC fn_glUniform4fv = nullptr;
|
||||
PFNGLUSEPROGRAMPROC fn_glUseProgram = nullptr;
|
||||
PFNGLACTIVETEXTUREPROC fn_glActiveTexture = nullptr;
|
||||
PFNGLGENERATEMIPMAPPROC fn_glGenerateMipmap = nullptr;
|
||||
PFNGLBUFFERDATAPROC fn_glBufferData = nullptr;
|
||||
PFNGLDRAWARRAYSINSTANCEDPROC fn_glDrawArraysInstanced = nullptr;
|
||||
PFNGLENABLEVERTEXATTRIBARRAYPROC fn_glEnableVertexAttribArray = nullptr;
|
||||
PFNGLVERTEXATTRIBDIVISORPROC fn_glVertexAttribDivisor = nullptr;
|
||||
PFNGLVERTEXATTRIBPOINTERPROC fn_glVertexAttribPointer = nullptr;
|
||||
PFNGLBINDRENDERBUFFERPROC fn_glBindRenderbuffer = nullptr;
|
||||
PFNGLDELETERENDERBUFFERSPROC fn_glDeleteRenderbuffers = nullptr;
|
||||
PFNGLFRAMEBUFFERRENDERBUFFERPROC fn_glFramebufferRenderbuffer = nullptr;
|
||||
PFNGLGENRENDERBUFFERSPROC fn_glGenRenderbuffers = nullptr;
|
||||
PFNGLRENDERBUFFERSTORAGEPROC fn_glRenderbufferStorage = nullptr;
|
||||
PFNGLFRAMEBUFFERTEXTUREPROC fn_glFramebufferTexture = nullptr;
|
||||
|
||||
namespace fn::gfx {
|
||||
|
||||
@@ -74,6 +85,17 @@ bool gl_loader_init() {
|
||||
LOAD(glUseProgram);
|
||||
LOAD(glActiveTexture);
|
||||
LOAD(glGenerateMipmap);
|
||||
LOAD(glBufferData);
|
||||
LOAD(glDrawArraysInstanced);
|
||||
LOAD(glEnableVertexAttribArray);
|
||||
LOAD(glVertexAttribDivisor);
|
||||
LOAD(glVertexAttribPointer);
|
||||
LOAD(glBindRenderbuffer);
|
||||
LOAD(glDeleteRenderbuffers);
|
||||
LOAD(glFramebufferRenderbuffer);
|
||||
LOAD(glGenRenderbuffers);
|
||||
LOAD(glRenderbufferStorage);
|
||||
LOAD(glFramebufferTexture);
|
||||
|
||||
#undef LOAD
|
||||
return true;
|
||||
|
||||
@@ -38,8 +38,22 @@
|
||||
extern PFNGLUNIFORM4FPROC fn_glUniform4f;
|
||||
extern PFNGLUNIFORM4FVPROC fn_glUniform4fv;
|
||||
extern PFNGLUSEPROGRAMPROC fn_glUseProgram;
|
||||
// Texture (gl_texture_load — issue 0026)
|
||||
extern PFNGLACTIVETEXTUREPROC fn_glActiveTexture;
|
||||
extern PFNGLGENERATEMIPMAPPROC fn_glGenerateMipmap;
|
||||
// Buffers / VAO data + draw + vertex attributes (graph_renderer)
|
||||
extern PFNGLBUFFERDATAPROC fn_glBufferData;
|
||||
extern PFNGLDRAWARRAYSINSTANCEDPROC fn_glDrawArraysInstanced;
|
||||
extern PFNGLENABLEVERTEXATTRIBARRAYPROC fn_glEnableVertexAttribArray;
|
||||
extern PFNGLVERTEXATTRIBDIVISORPROC fn_glVertexAttribDivisor;
|
||||
extern PFNGLVERTEXATTRIBPOINTERPROC fn_glVertexAttribPointer;
|
||||
// Renderbuffer / framebuffer texture
|
||||
extern PFNGLBINDRENDERBUFFERPROC fn_glBindRenderbuffer;
|
||||
extern PFNGLDELETERENDERBUFFERSPROC fn_glDeleteRenderbuffers;
|
||||
extern PFNGLFRAMEBUFFERRENDERBUFFERPROC fn_glFramebufferRenderbuffer;
|
||||
extern PFNGLGENRENDERBUFFERSPROC fn_glGenRenderbuffers;
|
||||
extern PFNGLRENDERBUFFERSTORAGEPROC fn_glRenderbufferStorage;
|
||||
extern PFNGLFRAMEBUFFERTEXTUREPROC fn_glFramebufferTexture; // sin "2D"
|
||||
|
||||
#define glAttachShader fn_glAttachShader
|
||||
#define glBindBuffer fn_glBindBuffer
|
||||
@@ -73,6 +87,17 @@
|
||||
#define glUseProgram fn_glUseProgram
|
||||
#define glActiveTexture fn_glActiveTexture
|
||||
#define glGenerateMipmap fn_glGenerateMipmap
|
||||
#define glBufferData fn_glBufferData
|
||||
#define glDrawArraysInstanced fn_glDrawArraysInstanced
|
||||
#define glEnableVertexAttribArray fn_glEnableVertexAttribArray
|
||||
#define glVertexAttribDivisor fn_glVertexAttribDivisor
|
||||
#define glVertexAttribPointer fn_glVertexAttribPointer
|
||||
#define glBindRenderbuffer fn_glBindRenderbuffer
|
||||
#define glDeleteRenderbuffers fn_glDeleteRenderbuffers
|
||||
#define glFramebufferRenderbuffer fn_glFramebufferRenderbuffer
|
||||
#define glGenRenderbuffers fn_glGenRenderbuffers
|
||||
#define glRenderbufferStorage fn_glRenderbufferStorage
|
||||
#define glFramebufferTexture fn_glFramebufferTexture
|
||||
#else
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
|
||||
@@ -3,7 +3,7 @@ name: gl_loader
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: gfx
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "bool gl_loader_init()"
|
||||
description: "Loader minimo de simbolos OpenGL 2.0+ para cross-compile a Windows. En Linux es no-op (simbolos resueltos via GL_GLEXT_PROTOTYPES). En Windows resuelve punteros con wglGetProcAddress. Redirige las llamadas con macros para que el codigo fuente sea portable."
|
||||
@@ -52,3 +52,21 @@ En Linux, el header activa `GL_GLEXT_PROTOTYPES` e incluye `<GL/gl.h>` + `<GL/gl
|
||||
1. Declarar `extern PFNGL<NAME>PROC fn_gl<Name>;` en el `.h`.
|
||||
2. Anadir `#define gl<Name> fn_gl<Name>` en el bloque `#ifdef _WIN32`.
|
||||
3. Instanciar el puntero en el `.cpp` y anadir `LOAD(gl<Name>);` dentro de `gl_loader_init()`.
|
||||
|
||||
## Cobertura `[v1.1]`
|
||||
|
||||
Funciones cubiertas (todas con macro `#define gl* fn_gl*` y `LOAD()` en el init):
|
||||
|
||||
| Grupo | Simbolos |
|
||||
|---|---|
|
||||
| Shaders / programs | `glCreateShader`, `glShaderSource`, `glCompileShader`, `glGetShaderiv`, `glGetShaderInfoLog`, `glCreateProgram`, `glAttachShader`, `glLinkProgram`, `glGetProgramiv`, `glGetProgramInfoLog`, `glUseProgram`, `glDeleteShader`, `glDeleteProgram` |
|
||||
| Uniforms | `glGetUniformLocation`, `glUniform1f`, `glUniform1i`, `glUniform2f`, `glUniform3f`, `glUniform4f`, `glUniform4fv` |
|
||||
| Buffers + VAO | `glGenBuffers`, `glBindBuffer`, `glDeleteBuffers`, `glBufferData`, `glGenVertexArrays`, `glBindVertexArray`, `glDeleteVertexArrays`, `glEnableVertexAttribArray`, `glVertexAttribPointer`, `glVertexAttribDivisor` |
|
||||
| Framebuffers + renderbuffers | `glGenFramebuffers`, `glBindFramebuffer`, `glDeleteFramebuffers`, `glFramebufferTexture`, `glFramebufferTexture2D`, `glGenRenderbuffers`, `glBindRenderbuffer`, `glDeleteRenderbuffers`, `glRenderbufferStorage`, `glFramebufferRenderbuffer` |
|
||||
| Draw | `glDrawArraysInstanced` (resto de `glDraw*` viene en `opengl32.dll`) |
|
||||
|
||||
`v1.1` (2026-04-25) anade los grupos **Buffers/VAO**, **Framebuffers/renderbuffers** y **Draw** para que `graph_renderer_cpp_viz` y otros consumidores compilen en cross-compile MinGW. Funciones de `opengl32.dll` 1.1 (`glClear`, `glEnable`, `glViewport`, `glDrawArrays`, etc.) se siguen resolviendo estaticamente — no necesitan loader.
|
||||
|
||||
## Compilador MinGW
|
||||
|
||||
El cross-compile a Windows requiere MinGW-w64 con thread model `-posix` para que `std::mutex` / `std::thread` funcionen (otros primitivos como `process_runner` y `toast` lo necesitan). Configurado en `cpp/toolchains/mingw-w64.cmake` via `x86_64-w64-mingw32-gcc-posix` / `g++-posix` + link static de `libwinpthread`.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "viz/graph_renderer.h"
|
||||
#include "viz/graph_types.h"
|
||||
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glext.h>
|
||||
// gl_loader: en Linux es no-op (incluye GL headers con GL_GLEXT_PROTOTYPES);
|
||||
// en Windows expone los punteros via #define gl* fn_gl* tras gl_loader_init().
|
||||
#include "gfx/gl_loader.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
@@ -3,7 +3,7 @@ name: graph_renderer
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: viz
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config)"
|
||||
description: "Renderer GPU de grafos con instanced rendering a FBO, compatible con ImGui::Image para visualizacion de grafos grandes"
|
||||
@@ -84,4 +84,8 @@ ndc = (screen / viewport) * 2 - 1
|
||||
|
||||
**Estado GL:** Guarda y restaura `GL_FRAMEBUFFER_BINDING` y `GL_VIEWPORT` para ser compatible con el render loop de ImGui sin efectos secundarios.
|
||||
|
||||
**Includes GL:** Usa `#define GL_GLEXT_PROTOTYPES` + `<GL/gl.h>` + `<GL/glext.h>`. Si el proyecto carga funciones GL via glad/gl3w, reemplazar estos includes por el loader correspondiente.
|
||||
**Includes GL:** Usa `gfx/gl_loader.h` (v1.1+). En Linux es no-op (incluye headers con `GL_GLEXT_PROTOTYPES`). En Windows expone los simbolos modernos via `wglGetProcAddress` con macros `#define gl* fn_gl*`. Cualquier app que use `graph_renderer` debe linkear `gl_loader.cpp` y llamar `fn::gfx::gl_loader_init()` una vez tras crear el contexto GL.
|
||||
|
||||
## Notas
|
||||
|
||||
- **v1.1** (2026-04-25): cambia de raw `<GL/glext.h>` a `gfx/gl_loader.h` para que compile en cross-compile MinGW. Sin cambios funcionales — el binario Linux es bit-equivalente.
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
#include "kpi_card.h"
|
||||
#include "sparkline.h"
|
||||
#include "core/tokens.h"
|
||||
#include "core/icons_tabler.h"
|
||||
#include <imgui.h>
|
||||
#include <cstdio>
|
||||
|
||||
void kpi_card(const char* label, float value, float delta_percent,
|
||||
const float* history, int history_count,
|
||||
const char* format) {
|
||||
const char* format,
|
||||
const char* icon) {
|
||||
using namespace fn_tokens;
|
||||
|
||||
// Card container — surface bg, border, rounded, padding.
|
||||
@@ -28,42 +30,40 @@ void kpi_card(const char* label, float value, float delta_percent,
|
||||
|
||||
// Altura fija (no AutoResizeY) para que:
|
||||
// (a) todas las cards de un grid queden alineadas visualmente,
|
||||
// (b) no haya recalculo de layout por card en cada resize de la ventana.
|
||||
// 78px alcanza para: label (~14px) + value (~22px con escala x1.4) + trend
|
||||
// (~14px) + padding sm*2 (~16px) ≈ 66px, +12px de aire.
|
||||
constexpr float card_height = 78.0f;
|
||||
// (b) no haya recalculo de layout por card en cada resize.
|
||||
constexpr float card_height = 86.0f;
|
||||
ImGui::BeginChild(child_id, ImVec2(width, card_height),
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
|
||||
// Label — muted
|
||||
// Top row: optional icon + label, ambos en text_muted.
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
|
||||
if (icon && *icon) {
|
||||
ImGui::TextUnformatted(icon);
|
||||
ImGui::SameLine(0, spacing::xs);
|
||||
}
|
||||
ImGui::TextUnformatted(label);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Value — escala compacta 1.4x, proporcional a una card de 78px.
|
||||
// El format controla el sufijo (ej: "%.0f%%" para porcentajes).
|
||||
// Value — escala compacta 1.4x, proporcional a una card de 86px.
|
||||
ImGui::SetWindowFontScale(1.4f);
|
||||
char value_buf[64];
|
||||
std::snprintf(value_buf, sizeof(value_buf), format, value);
|
||||
ImGui::TextUnformatted(value_buf);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
// Delta / trend — SIEMPRE se reserva la linea aunque no haya tendencia,
|
||||
// para que todas las cards tengan la misma altura. Cuando no hay delta
|
||||
// ni history, se muestra un guion en text_dim para mantener el ritmo
|
||||
// visual sin hacer ruido con "+0.0%".
|
||||
// Delta / trend — SIEMPRE se reserva la linea aunque no haya tendencia.
|
||||
const bool has_delta = delta_percent != 0.0f;
|
||||
const bool has_history = history != nullptr && history_count > 0;
|
||||
|
||||
if (has_delta) {
|
||||
const bool positive = delta_percent >= 0.0f;
|
||||
const ImVec4 delta_color = positive ? colors::success : colors::error;
|
||||
char delta_buf[32];
|
||||
char delta_buf[48];
|
||||
if (positive) {
|
||||
std::snprintf(delta_buf, sizeof(delta_buf), "\xe2\x96\xb2 +%.1f%%", delta_percent);
|
||||
std::snprintf(delta_buf, sizeof(delta_buf), TI_TRENDING_UP " +%.1f%%", delta_percent);
|
||||
} else {
|
||||
std::snprintf(delta_buf, sizeof(delta_buf), "\xe2\x96\xbc %.1f%%", delta_percent);
|
||||
std::snprintf(delta_buf, sizeof(delta_buf), TI_TRENDING_DOWN " %.1f%%", delta_percent);
|
||||
}
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, delta_color);
|
||||
ImGui::TextUnformatted(delta_buf);
|
||||
@@ -73,12 +73,11 @@ void kpi_card(const char* label, float value, float delta_percent,
|
||||
sparkline(label, history, history_count, delta_color, 120.0f, 24.0f);
|
||||
}
|
||||
} else if (has_history) {
|
||||
// Sin delta pero con historia: sparkline en primary (neutro).
|
||||
sparkline(label, history, history_count, colors::primary, 120.0f, 24.0f);
|
||||
} else {
|
||||
// Placeholder para preservar altura de la card.
|
||||
// Placeholder para preservar altura.
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
|
||||
ImGui::TextUnformatted("\xe2\x80\x94"); // em dash
|
||||
ImGui::TextUnformatted(TI_MINUS);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
// KPI card — displays a key metric with trend.
|
||||
// Usage:
|
||||
// #include "core/icons_tabler.h"
|
||||
// float history[] = {10, 12, 11, 15, 18, 17, 20};
|
||||
// kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f");
|
||||
// kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f", TI_CASH);
|
||||
//
|
||||
// Shows:
|
||||
// - Label (small, muted)
|
||||
// - Optional icon (Tabler glyph) + label (small, muted) on top row
|
||||
// - Value (large font)
|
||||
// - Delta badge (green up / red down)
|
||||
// - Delta badge (green TI_TRENDING_UP / red TI_TRENDING_DOWN)
|
||||
// - Sparkline of history
|
||||
|
||||
void kpi_card(const char* label, float value, float delta_percent,
|
||||
const float* history = nullptr, int history_count = 0,
|
||||
const char* format = "%.1f");
|
||||
const char* format = "%.1f",
|
||||
const char* icon = nullptr);
|
||||
|
||||
@@ -3,11 +3,11 @@ name: kpi_card
|
||||
kind: component
|
||||
lang: cpp
|
||||
domain: viz
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
purity: pure
|
||||
signature: "void kpi_card(const char* label, float value, float delta_percent, const float* history = nullptr, int history_count = 0, const char* format = \"%.1f\")"
|
||||
description: "Card de KPI con valor grande, delta porcentual y sparkline historico. Contenedor con surface bg, borde y radius via tokens (Mantine Paper equivalente)."
|
||||
tags: [imgui, kpi, card, dashboard, metrics, sparkline, tokens]
|
||||
signature: "void kpi_card(const char* label, float value, float delta_percent, const float* history = nullptr, int history_count = 0, const char* format = \"%.1f\", const char* icon = nullptr)"
|
||||
description: "Card de KPI con icono opcional + label, valor grande, delta porcentual con TI_TRENDING_UP/DOWN y sparkline historico. Contenedor con surface bg, borde y radius via tokens (Mantine Paper equivalente)."
|
||||
tags: [imgui, kpi, card, dashboard, metrics, sparkline, tokens, tabler]
|
||||
uses_functions: ["sparkline_cpp_viz", "tokens_cpp_core"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
@@ -32,7 +32,9 @@ params:
|
||||
desc: "Numero de valores en el array history"
|
||||
- name: format
|
||||
desc: "Formato printf para el valor principal (ej: \"$%.0f\", \"%.1f%%\", \"%.2f\")"
|
||||
output: "Renderiza la card KPI completa en el frame ImGui actual: label muted, valor grande, badge delta verde/rojo con triangulo, y sparkline de 120x24px"
|
||||
- name: icon
|
||||
desc: "Glyph Tabler opcional (TI_* de core/icons_tabler.h) renderizado antes del label. Nullable — si es nullptr solo muestra label"
|
||||
output: "Renderiza la card KPI completa en el frame ImGui actual: top row con icono opcional + label muted, valor grande, badge delta verde/rojo con TI_TRENDING_UP/DOWN, y sparkline de 120x24px"
|
||||
---
|
||||
|
||||
# kpi_card
|
||||
@@ -66,7 +68,8 @@ ImGui::Columns(1);
|
||||
|
||||
- **v1.1**: la card se renderiza dentro de un `BeginChild` con `surface` bg, `border` y `radius::md` de `fn_tokens` — replica el `<Paper withBorder radius="md" p="sm">` del frontend.
|
||||
- **v1.2**: altura fija 78px (antes 108px) + font scale `1.4x` (antes `1.8x`) + padding `spacing::sm` (antes `md`). Mas compacta para densidades altas de KPIs. `NoScrollbar|NoScrollWithMouse` ademas de altura fija para evitar lag al redimensionar.
|
||||
- **v1.3** (sesion 2026-04-25): nuevo parametro opcional `icon` (Tabler `TI_*` glyph) renderizado antes del label en la top row. Triangulos de delta migrados a `TI_TRENDING_UP` / `TI_TRENDING_DOWN` (los UTF-8 hex anteriores no estan en el atlas Karla/DroidSans → cuadritos). Em dash placeholder migrado a `TI_MINUS`. Altura subida 78→86 px para acomodar el row icono+label sin apretar. `uses_functions` ahora incluye implicitamente `icons_tabler` (header puro, no funcion).
|
||||
- El ancho se adapta al contenedor padre via `GetContentRegionAvail().x`. Para que ocupe exactamente una celda usar `ImGui::BeginTable` — `BeginGroup` / `dashboard_grid` no propagan ancho constrained y la card desbordaria la celda.
|
||||
- La linea de trend siempre se reserva (delta, sparkline o em dash placeholder en `text_dim`) para que un grid de KPIs quede alineado vertical.
|
||||
- Los caracteres UTF-8 del triangulo (`▲` U+25B2 y `▼` U+25BC) y del em dash (`—` U+2014) requieren que la fuente ImGui tenga el rango de simbolos geometricos / puntuacion general cargado.
|
||||
- Colores: delta usa `fn_tokens::colors::{success, error}`, placeholder em dash usa `text_dim`, label usa `text_muted`.
|
||||
- ~~Los caracteres UTF-8 del triangulo (`▲` U+25B2 y `▼` U+25BC) y del em dash (`—` U+2014) requieren que la fuente ImGui tenga el rango de simbolos geometricos / puntuacion general cargado.~~ → Obsoleto en v1.3: ahora se usan glyphs Tabler que estan en el atlas mergeado por `icon_font_cpp_core`.
|
||||
- Colores: delta usa `fn_tokens::colors::{success, error}`, placeholder `TI_MINUS` usa `text_dim`, label + icono usan `text_muted`.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
set(CMAKE_SYSTEM_NAME Windows)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
|
||||
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
|
||||
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
|
||||
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc-posix)
|
||||
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++-posix)
|
||||
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
||||
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
||||
@@ -13,5 +13,6 @@ set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Static link runtime so .exe is self-contained
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libgcc -static-libstdc++")
|
||||
# Static link runtime so .exe is self-contained. Con el compilador -posix
|
||||
# tambien hay que statificar winpthread para no depender de libwinpthread-1.dll.
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libgcc -static-libstdc++ -static -lwinpthread")
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
| [0022](completed/0022-init-pipelines.md) | Init Pipelines (scaffolding) | completado | alta | feature | — |
|
||||
| [0023](completed/0023-testing-utils.md) | Testing Utilities Go | completado | media | feature | — |
|
||||
| [0024](completed/0024-dashboard-yaml-split-por-tab.md) | auto_metabase: split dashboard YAMLs por tab | completado | alta | mejora | — |
|
||||
| [0025](0025-cpp-text-editor-file-watcher.md) | C++ text_editor + file_watcher | pendiente | alta | feature | — |
|
||||
| [0026](0026-cpp-gl-texture-load.md) | C++ gl_texture_load (stb_image → sampler2D) | pendiente | alta | feature | 0035, 0036 |
|
||||
| [0025](completed/0025-cpp-text-editor-file-watcher.md) | C++ text_editor + file_watcher | completado | alta | feature | — |
|
||||
| [0026](completed/0026-cpp-gl-texture-load.md) | C++ gl_texture_load (stb_image → sampler2D) | completado | alta | feature | 0035, 0036 |
|
||||
| [0027](0027-cpp-gl-compute-pingpong.md) | C++ gl_compute_shader + gl_pingpong_fbo + DAG Compute | pendiente | alta | feature | — |
|
||||
| [0028](0028-cpp-implot3d-3d-viz.md) | C++ ImPlot3D + surface_plot_3d + scatter_3d | pendiente | media | feature | — |
|
||||
| [0029](0029-cpp-mesh-viewer.md) | C++ mesh_viewer + obj loader + orbit_camera | pendiente | media | feature | — |
|
||||
|
||||
@@ -224,3 +224,58 @@ Este proyecto se instala identico en cualquier maquina con el registry clonado:
|
||||
3. Build del dashboard.
|
||||
|
||||
Los datos son los `.db` locales — cada PC ve su propio estado del registry y sus propias `operations.db`. No hay sincronizacion remota de datos en este servicio: para eso existe `fn sync` contra `registry_api` (proyecto diferente, ver memoria `project_registry_api`).
|
||||
|
||||
---
|
||||
|
||||
## Estado actual
|
||||
|
||||
### Fase — projects view + mutaciones desde el dashboard `[done 2026-04-25]`
|
||||
|
||||
El dashboard pasa de read-only a manipular el registry via la API. Ampliacion en tres patas:
|
||||
|
||||
**Backend (`sqlite_api`)** — endpoints nuevos en `handlers_projects.go` y `handlers_mutations.go`:
|
||||
|
||||
| Metodo | Path | Que hace |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/projects` | Lista con conteos `apps_count` / `analyses_count` / `vaults_count` por proyecto + bloque `orphans` (entidades con `project_id` vacio). |
|
||||
| `GET` | `/api/projects/{id}` | Detalle: apps[], analyses[], vaults[]. Acepta `id="orphans"` para devolver las huerfanas. |
|
||||
| `POST` | `/api/reindex` | Ejecuta `fn index` desde `registryRoot`, devuelve `{ok, output}`. |
|
||||
| `POST` | `/api/add/app` | Body `{name, lang, domain, project, description}` → crea `apps/{name}/` o `projects/{p}/apps/{name}/` con `app.md` minimo + `fn index`. |
|
||||
| `POST` | `/api/add/analysis` | Body `{name, project, packages[], description}` → invoca `fn run init_jupyter_analysis [--project p] name pkg1 pkg2 ...`. |
|
||||
| `POST` | `/api/add/vault` | Body `{name, project, path, description}` → crea dir o symlink en `projects/{p}/vaults/` + entry append en `vault.yaml`. |
|
||||
|
||||
`Server.registryRoot` se inyecta en `NewServer(pool, root)` (rebajado de `findRegistryRoot()` en `main.go`). Helpers `runFN()` y `runShell()` ejecutan con `cmd.Dir = registryRoot` y `FN_REGISTRY_ROOT` en el env.
|
||||
|
||||
**Dashboard (`registry_dashboard`)** — actions bar + tab Projects + modal Add:
|
||||
|
||||
- Toolbar nueva en el header (`fn_ui::toolbar`): boton `Reindex` (Primary) → dispara `http_post_reindex` via `process_runner`; boton `+ Add` → abre `modal_dialog`; boton `Reload`; `toast_inbox_button` con badge.
|
||||
- Modal Add con `select` para kind (App / Analysis / Vault), `select` de proyecto (obligatorio para Vault, opcional para resto), `text_input` Name + Description y campos especificos por kind (lang/domain para App, packages CSV para Analysis, abs path para Vault). Submit dispara el endpoint correspondiente via `process_runner`. Toast al completar + reload automatico.
|
||||
- Tab Projects con dos columnas: `tree_view` izquierda (proyectos + entrada "(orphans)" cuando hay entidades huerfanas), detalle derecha con tabs internas Apps / Analysis / Vaults. Click en un proyecto dispara `load_project_detail_http`.
|
||||
|
||||
**Datos en `RegistryData`**: nuevos `projects[]`, `orphan_apps`, `orphan_analyses`, `orphan_vaults`. Tipos nuevos `ProjectRow`, `VaultRow`, `ProjectDetail`. `load_registry_data_http` llama a `load_projects_http` al final como best-effort (no fatal si falla).
|
||||
|
||||
### Bug fix — vibracion al redimensionar `[done 2026-04-25]`
|
||||
|
||||
Dos fuentes de "vibracion" durante drag-resize de la ventana:
|
||||
|
||||
- `fullscreen_window_cpp_core` v0.2: anadido `NoScrollbar | NoScrollWithMouse`. Sin esto, si el contenido excedia por 1-2px aparecia un scrollbar fugaz que reducia el ancho ~14px y reflowaba todo.
|
||||
- `views.cpp::draw_dashboard`: altura de charts pasa de `GetContentRegionAvail().y * 0.35` a constante 260 px. La proporcion relativa propagaba el resize a todos los plots.
|
||||
- `kpi_card_cpp_viz` v1.2: altura fija 78 px (antes 108) + scale 1.4x (antes 1.8) + padding sm + `NoScrollbar`. El `AutoResizeY` con 8 cards generaba lag perceptible al redimensionar.
|
||||
|
||||
### Bug fix — HTTP POST timeout en thread de background `[done 2026-04-25]`
|
||||
|
||||
`http_client.cpp::request()` pasaba `struct timeval` a `setsockopt(SO_RCVTIMEO)` en Windows, donde MSDN especifica `DWORD` ms. Resultado: timeout efectivo de **5 ms** en lugar de **5 s**. Se nota especialmente en POST desde threads (background runners) porque la latencia de scheduling puede pasar de 5 ms. Fix: rama `_WIN32` con `DWORD timeout_ms`. Tambien `wsa_init` envuelto en `std::call_once` para evitar race entre main thread + runners. Mensajes de error formateados con ASCII (em dash U+2014 falla render con la fuente default).
|
||||
|
||||
### Tooling sibling — primitives_gallery `[done 2026-04-25]`
|
||||
|
||||
Nueva app dev en `cpp/apps/primitives_gallery/` (no es app del registry, vive en el source tree). Catalogo visual interactivo de los 19 primitivos UI de `cpp/functions/{core,viz}` con sidebar + panel + snippet por demo. Doble rol: smoke test visual al modificar tokens/componentes y build gate (esta en el CMake principal — si un primitivo rompe API la gallery no compila).
|
||||
|
||||
Demo destacada: `graph_viewport` con sliders de Nodes (100-20 000), Clusters (2-16) y los tres parametros de `ForceLayoutConfig` (Repulsion / Attraction / Gravity) aplicados en vivo. Util tambien como benchmark de rendimiento del stack `graph_renderer` + `graph_force_layout` + `graph_spatial_hash`.
|
||||
|
||||
`README.md` propio en `cpp/apps/primitives_gallery/README.md`.
|
||||
|
||||
### Lo siguiente que pega
|
||||
|
||||
- Tests unitarios de logica pura (Phase A del plan de tests): vendoreado de `doctest`, ~6 tests para `label_stride`, `slice_at`, `process_runner` transitions, `toast` queue, `tokens` sanity, `parse_url`. Cierra el ciclo gallery (visual) + ctest (logica).
|
||||
- Para que algunos tests sean posibles hace falta exponer funciones internas de `bar_chart.cpp` y `pie_chart.cpp` (actualmente en namespace anonimo).
|
||||
- `loginctl enable-linger lucas` para que el `sqlite_api.service` (user-level systemd) sobreviva al logout. Requiere sudo una vez. Decision pendiente del usuario.
|
||||
|
||||
Reference in New Issue
Block a user