commit 4a09ed9404366e5a9e3bb857aa455a77f59d7c82 Author: fn-registry agent Date: Fri May 8 00:07:55 2026 +0200 chore: sync from fn-registry agent diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..cc25d77 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,35 @@ +add_imgui_app(shaders_lab + main.cpp + compiler.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/gl_loader.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/gl_shader.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/gl_framebuffer.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/fullscreen_quad.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/shader_canvas.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/uniform_parser.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/uniform_panel.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/dag_catalog.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/dag_compile.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/dag_uniforms.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/dag_panel.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/dag_node_editor.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/dag_palette.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/dag_node_previews.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/shaderlab_db.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/code_to_generator.cpp + # Primitivos UI usados por el modal Save-as-generator. + ${CMAKE_SOURCE_DIR}/functions/core/modal_dialog.cpp + ${CMAKE_SOURCE_DIR}/functions/core/text_input.cpp + ${CMAKE_SOURCE_DIR}/functions/core/button.cpp + # fps_overlay, panel_menu, layouts_menu, app_menubar, layout_storage ya + # viven en fn_framework. +) +target_include_directories(shaders_lab PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) +target_link_libraries(shaders_lab PRIVATE imgui_node_editor SQLite::SQLite3) + +if(WIN32) + # GUI app: sin consola al lanzar (subsystem:windows / -mwindows) + set_target_properties(shaders_lab PROPERTIES WIN32_EXECUTABLE TRUE) +endif() diff --git a/app.md b/app.md new file mode 100644 index 0000000..3e5ecc2 --- /dev/null +++ b/app.md @@ -0,0 +1,104 @@ +--- +name: shaders_lab +lang: cpp +domain: gfx +description: "Live GLSL shader playground con DAG pipeline. Editor de codigo con compilacion en caliente, panel DAG con paleta de generadores/filtros/output, dos canvas (Code y DAG), parseo de uniforms anotados (// @slider, @color, @xy) que se convierten en controles, persistencia de generators en shaders_lab.db, y guardado/carga de layouts ImGui." +tags: [imgui, opengl, glsl, shaders, dag, live-coding, playground, sqlite] +uses_functions: + # gfx + - gl_loader_cpp_gfx + - gl_shader_cpp_gfx + - gl_framebuffer_cpp_gfx + - fullscreen_quad_cpp_gfx + - shader_canvas_cpp_gfx + - uniform_parser_cpp_gfx + - uniform_panel_cpp_gfx + - dag_catalog_cpp_gfx + - dag_compile_cpp_gfx + - dag_uniforms_cpp_gfx + - dag_panel_cpp_gfx + - dag_node_editor_cpp_gfx + - dag_palette_cpp_gfx + - dag_node_previews_cpp_gfx + - shaderlab_db_cpp_gfx + - code_to_generator_cpp_gfx + # core (modal Save-as-generator) + - modal_dialog_cpp_core + - text_input_cpp_core + - button_cpp_core +uses_types: + - dag_types_cpp_gfx +framework: "imgui" +entry_point: "main.cpp" +dir_path: "cpp/apps/shaders_lab" +repo_url: "" +--- + +## Arquitectura + +App ImGui de live-coding GLSL con dos modos en paralelo: + +1. **Code panel** — editor de fragment shader libre. Las anotaciones en + uniforms (`// @slider`, `// @color`, `// @xy`, `// @toggle`) se parsean y + convierten en controles del panel **Controls** que escriben en un + `UniformStore` aplicado al programa cada frame. +2. **DAG panel** — pipeline node-based con catalogo de generadores + (plasma, voronoi, etc.) y filtros (blur, threshold, etc.) que se + compilan a un fragment shader unificado y se renderizan en **Canvas DAG**. + +Al guardar un Code shader como "generator" se traduce a un `DagNodeDef` y se +persiste en `shaders_lab.db` (tabla via `shaderlab_db`), apareciendo en la +paleta del DAG junto a los builtins. + +## Capas + +| Archivo | Responsabilidad | +|---|---| +| `main.cpp` | UI shell, paneles, modal save-as, layouts, AppConfig | +| `compiler.cpp` | `compile_code()`, `compile_dag()`, `mark_code_dirty()` con debounce 250ms | + +`main.cpp` mantiene estado global de sesion (g_source, g_pipeline, g_descs, +g_store, g_layouts...) — ImGui retained-mode obliga a que persista entre +frames. Toda la logica pura de compilacion vive en `compiler.cpp` y en las +funciones `dag_compile`, `code_to_generator`, `uniform_parser` del registry. + +## Persistencia + +- **`shaders_lab.db`** (junto al .exe) — tabla de generators de usuario via + `shaderlab_db_*`, ademas de `imgui_layouts` (creada por `layout_storage`). +- `imgui.ini` y `app_settings.ini` — gestionados por `fn::run_app` en + `/local_files/`. + +## Paneles + +| Panel | Atajo | Que muestra | +|---|---|---| +| Code | Ctrl+1 | Editor del fragment shader + boton "Save as generator" | +| DAG Pipeline | Ctrl+2 | Node editor con la pipeline | +| Canvas Code | Ctrl+3 | Render del Code shader | +| Canvas DAG | Ctrl+4 | Render del shader compilado del DAG | +| Controls | Ctrl+5 | Sliders/color pickers de uniforms anotados | +| Functions | Ctrl+6 | Paleta del DAG (generators + filters + output) | +| Generated GLSL | Ctrl+7 | GLSL final del DAG con uniforms baked como const array | + +## Build + +```bash +# Linux +cd cpp && cmake -B build/linux -S . && cmake --build build/linux --target shaders_lab + +# Windows (cross-compile) +cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w64.cmake \ + && cmake --build build/windows --target shaders_lab +``` + +## Decisiones + +- `init_gl_loader = true` (via `fn::run_app` por default cuando se enlaza + con OpenGL) — `shader_canvas`, `gl_shader`, `gl_framebuffer` llaman gl*. +- `viewports = true` — los Canvas se pueden arrastrar fuera del main. +- DAG default: arranca con un nodo "plasma" + "output" si la paleta los + encuentra; persiste el INI con `layout_storage`. +- El boton "Save as generator" valida snake_case, evita colisionar con + builtins, traduce con `code_to_generator`, persiste con `shaderlab_db_save_generator`, + y registra el nodo nuevo en el catalogo en vivo (`dag_register_node`). diff --git a/compiler.cpp b/compiler.cpp new file mode 100644 index 0000000..1f2f5d5 --- /dev/null +++ b/compiler.cpp @@ -0,0 +1,63 @@ +#include "compiler.h" + +#include "gfx/shader_canvas.h" +#include "gfx/gl_shader.h" +#include "gfx/uniform_parser.h" +#include "gfx/uniform_panel.h" +#include "gfx/dag_compile.h" +#include "gfx/dag_uniforms.h" + +#include +#include +#include + +// ── Globals declarados en main.cpp (single source of truth) ───────────────── +extern fn::gfx::ShaderCanvas g_canvas_code; +extern fn::gfx::ShaderCanvas g_canvas_dag; +extern std::string g_source; +extern std::string g_code_err; +extern int g_code_err_line; +extern std::chrono::steady_clock::time_point g_code_last_edit; +extern bool g_code_dirty; +extern std::vector g_descs; +extern fn::gfx::UniformStore g_store; +extern std::vector g_pipeline; +extern std::string g_dag_glsl; +extern std::string g_dag_err; +extern int g_dag_err_line; + +namespace shaders_lab { + +void compile_code() { + auto r = fn::gfx::compile_fragment(g_source); + if (r.ok) { + g_descs = fn::gfx::parse_uniforms(g_source); + fn::gfx::uniforms_sync(g_store, g_descs); + fn::gfx::canvas_set_program(g_canvas_code, r.program); + g_code_err.clear(); + g_code_err_line = -1; + } else { + g_code_err = r.err_msg; + g_code_err_line = r.err_line; + } +} + +void compile_dag() { + g_dag_glsl = fn::gfx::compile_dag_to_glsl(g_pipeline); + auto r = fn::gfx::compile_fragment(g_dag_glsl); + if (r.ok) { + fn::gfx::canvas_set_program(g_canvas_dag, r.program); + g_dag_err.clear(); + g_dag_err_line = -1; + } else { + g_dag_err = r.err_msg; + g_dag_err_line = r.err_line; + } +} + +void mark_code_dirty() { + g_code_last_edit = std::chrono::steady_clock::now(); + g_code_dirty = true; +} + +} // namespace shaders_lab diff --git a/compiler.h b/compiler.h new file mode 100644 index 0000000..44d4f3f --- /dev/null +++ b/compiler.h @@ -0,0 +1,24 @@ +#pragma once + +// shaders_lab/compiler — extrae las rutinas impuras de compilacion del shader +// (compile_code, compile_dag, mark_code_dirty) desde main.cpp para que el +// archivo principal quede acotado a la composicion de paneles ImGui. +// +// Las globals (g_source, g_descs, g_store, g_pipeline, etc.) se declaran +// extern y viven en main.cpp; aqui solo orquestamos compilacion. + +namespace shaders_lab { + +// Compila g_source -> programa OpenGL para g_canvas_code, refresca g_descs +// y sincroniza g_store. Actualiza g_code_err / g_code_err_line. +void compile_code(); + +// Compila g_pipeline -> g_dag_glsl -> programa OpenGL para g_canvas_dag. +// Actualiza g_dag_err / g_dag_err_line. +void compile_dag(); + +// Marca el shader Code como dirty y registra el timestamp del ultimo edit +// (para debounce de 250ms en el render loop). +void mark_code_dirty(); + +} // namespace shaders_lab diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..ea1f4bf --- /dev/null +++ b/main.cpp @@ -0,0 +1,428 @@ +#include "app_base.h" +#include "imgui.h" + +#include "gfx/shader_canvas.h" +#include "gfx/gl_shader.h" +#include "gfx/uniform_parser.h" +#include "gfx/uniform_panel.h" +#include "gfx/dag_catalog.h" +#include "gfx/dag_compile.h" +#include "gfx/dag_uniforms.h" +#include "gfx/dag_panel.h" +#include "gfx/dag_node_editor.h" +#include "gfx/dag_palette.h" +#include "gfx/dag_node_previews.h" +#include "gfx/code_to_generator.h" +#include "gfx/shaderlab_db.h" +#include "core/panel_menu.h" +#include "core/layouts_menu.h" +#include "core/layout_storage.h" +#include "core/modal_dialog.h" +#include "core/text_input.h" +#include "core/button.h" +#include "core/tokens.h" + +#include "compiler.h" + +#include +#include +#include +#include +#include +#include + +// Globals: linked extern desde compiler.cpp. NO `static` aqui. +fn::gfx::ShaderCanvas g_canvas_code; +fn::gfx::ShaderCanvas g_canvas_dag; + +// Default placeholder so the Code panel does something useful on first launch +// without committing to one specific look. +static const char* CODE_PLACEHOLDER = R"glsl(// Escribe tu fragment shader aqui. +// Declara uniforms con anotaciones (// @slider, @color, @xy) +// para que aparezcan como controles al guardar como generator. + +uniform vec3 u_color; // @color default=0.5,0.2,0.8 +uniform float u_speed; // @slider min=0.1 max=5 default=1 + +void main() { + vec2 uv = gl_FragCoord.xy / u_resolution; + float t = u_time * u_speed; + vec3 c = u_color * (0.5 + 0.5 * cos(t + uv.xyx + vec3(0.0, 2.0, 4.0))); + fragColor = vec4(c, 1.0); +} +)glsl"; + +std::string g_source = CODE_PLACEHOLDER; +std::string g_code_err; +int g_code_err_line = -1; +std::chrono::steady_clock::time_point g_code_last_edit; +bool g_code_dirty = true; +std::vector g_descs; +fn::gfx::UniformStore g_store; + +std::vector g_pipeline; +std::string g_dag_glsl; +std::string g_dag_err; +int g_dag_err_line = -1; +static bool g_dag_dirty = true; // solo lo usa main.cpp + +// ── Panel visibility (toggled from View menu and panel close button) ────── +static bool g_show_code = true; +static bool g_show_dag = true; +static bool g_show_canvas_c = true; +static bool g_show_canvas_d = true; +static bool g_show_controls = true; +static bool g_show_functions = true; +static bool g_show_generated = true; + +// Tabla de paneles toggleables que fn::run_app pasa a app_menubar cada frame. +// Vive en el ambito del archivo para poder referenciarse desde main(). +static constexpr fn_ui::PanelToggle k_panels[] = { + {"Code", "Ctrl+1", &g_show_code}, + {"DAG Pipeline", "Ctrl+2", &g_show_dag}, + {"Canvas Code", "Ctrl+3", &g_show_canvas_c}, + {"Canvas DAG", "Ctrl+4", &g_show_canvas_d}, + {"Controls", "Ctrl+5", &g_show_controls}, + {"Functions", "Ctrl+6", &g_show_functions}, + {"Generated GLSL","Ctrl+7", &g_show_generated}, +}; + +// ── Layouts (named ImGui ini snapshots persisted in shaders_lab.db) ─────── +// El storage opaco encapsula la BD y el blob pendiente. Los callbacks +// envuelven save/apply/delete/reset y se pasan a app_menubar tal cual. +static fn_ui::LayoutStorage* g_layouts = nullptr; +static fn_ui::LayoutCallbacks g_layout_cb; + +// ── Save-as-generator modal state ───────────────────────────────────────── +static bool g_save_modal_open = false; +static char g_save_name[64] = "my_shader"; +static char g_save_label[64] = "my shader"; +static char g_save_desc[256] = ""; +static char g_save_tags[128] = "shaders_lab,user"; +static std::string g_save_err; + +// compile_code, compile_dag, mark_code_dirty viven en compiler.cpp +using shaders_lab::compile_code; +using shaders_lab::compile_dag; +using shaders_lab::mark_code_dirty; + +static void ensure_dag_default() { + if (g_pipeline.empty()) { + const fn::gfx::DagNodeDef* plasma = fn::gfx::dag_find("plasma"); + if (plasma) { + fn::gfx::DagStep s; + s.id = "n_plasma"; + s.name = plasma->name; + s.params = plasma->param_defaults; + g_pipeline.push_back(s); + } + } + bool has_output = false; + for (const auto& s : g_pipeline) { + const fn::gfx::DagNodeDef* d = fn::gfx::dag_find(s.name); + if (d && d->kind == fn::gfx::DagKind::Output) { has_output = true; break; } + } + if (!has_output) { + const fn::gfx::DagNodeDef* out = fn::gfx::dag_find("output"); + if (out) { + fn::gfx::DagStep s; + s.id = "n_output"; + s.name = out->name; + s.editor_pos_x = 500.0f; + if (!g_pipeline.empty()) s.source_ids[0] = g_pipeline.front().id; + g_pipeline.push_back(s); + } + } +} + +static void draw_err(const std::string& msg, int line) { + if (msg.empty()) return; + ImGui::Separator(); + if (line > 0) { + ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "line %d: %s", line, msg.c_str()); + } else { + ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", msg.c_str()); + } +} + +// snake_case validation: lowercase letters, digits, underscores; first char a-z. +static bool valid_id(const char* s) { + if (!s || !*s) return false; + if (!(*s >= 'a' && *s <= 'z')) return false; + for (const char* p = s; *p; ++p) { + char c = *p; + if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_')) return false; + } + return true; +} + +// Build a DagNodeDef from current Code source + form fields, persist it, and +// register in the live catalog. Returns "" on success or an error message. +static std::string save_current_as_generator() { + if (!valid_id(g_save_name)) return "name must be snake_case (a-z, 0-9, _) and start with a letter"; + + if (fn::gfx::dag_find(g_save_name)) { + const fn::gfx::DagNodeDef* existing = fn::gfx::dag_find(g_save_name); + if (existing && existing->is_builtin) { + return std::string("name '") + g_save_name + "' collides with a built-in node"; + } + // user node with same name → overwrite is allowed + } + + auto tr = fn::gfx::code_to_generator(g_source); + if (!tr.ok) return tr.err; + + fn::gfx::GeneratorRecord rec; + rec.id = g_save_name; + rec.label = g_save_label[0] ? g_save_label : g_save_name; + rec.description = g_save_desc; + rec.source_glsl = g_source; + rec.body_glsl = tr.body_template; + rec.param_count = tr.param_count; + rec.param_defaults = tr.param_defaults; + rec.param_names = tr.param_names; + rec.controls = tr.controls; + rec.tags = g_save_tags; + + std::string err; + if (!fn::gfx::shaderlab_db_save_generator(rec, &err)) { + return std::string("db save failed: ") + err; + } + + fn::gfx::DagNodeDef def = fn::gfx::make_generator_def(rec.id, rec.label, rec.description, tr); + if (!fn::gfx::dag_register_node(def)) { + return std::string("could not register node '") + rec.id + "'"; + } + return ""; +} + +// Reconstitute every persisted generator and inject it into the live catalog. +static void load_user_generators_into_catalog() { + for (const auto& rec : fn::gfx::shaderlab_db_list_generators()) { + // Re-translate body_template from source to keep the lambda fresh. + // (We could trust rec.body_glsl, but re-running ensures forward-compat + // when we tweak the translator.) + auto tr = fn::gfx::code_to_generator(rec.source_glsl); + if (!tr.ok) continue; // skip broken records + + fn::gfx::DagNodeDef def = fn::gfx::make_generator_def(rec.id, rec.label, rec.description, tr); + fn::gfx::dag_register_node(def); + } +} + +static void render() { + // Apply pending layout BEFORE any ImGui::Begin this frame. + // (LoadIniSettingsFromMemory must happen before windows are submitted.) + std::string applied = fn_ui::layout_storage_apply_pending(g_layouts); + if (!applied.empty()) g_layout_cb.active_name = applied; + + if (!g_canvas_code.initialized) fn::gfx::canvas_init(g_canvas_code); + if (!g_canvas_dag.initialized) fn::gfx::canvas_init(g_canvas_dag); + + if (g_code_dirty) { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - g_code_last_edit).count(); + if (elapsed > 250) { + compile_code(); + g_code_dirty = false; + } + } + if (g_dag_dirty) { + compile_dag(); + g_dag_dirty = false; + } + + ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport()); + + // Menubar (View + Layouts + Settings + About) la invoca fn::run_app a + // partir de AppConfig::panels y AppConfig::layouts_cb. + + // --- Code window --- + if (g_show_code) { + if (ImGui::Begin("Code", &g_show_code)) { + if (fn_ui::button("Save as generator...", fn_ui::ButtonVariant::Secondary)) { + g_save_modal_open = true; + g_save_err.clear(); + } + + if (fn_ui::modal_dialog_begin("Save as generator", &g_save_modal_open, + ImVec2(420, 0))) { + ImGui::TextUnformatted("Guardar shader actual como nodo Gen del DAG."); + ImGui::Spacing(); + fn_ui::text_input("name (snake_case)", g_save_name, sizeof(g_save_name), + "ej: my_shader"); + fn_ui::text_input("label", g_save_label, sizeof(g_save_label)); + ImGui::InputTextMultiline("description", g_save_desc, sizeof(g_save_desc), + ImVec2(380, 60)); + fn_ui::text_input("tags (CSV)", g_save_tags, sizeof(g_save_tags)); + + if (!g_save_err.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); + ImGui::TextWrapped("%s", g_save_err.c_str()); + ImGui::PopStyleColor(); + } + + ImGui::Separator(); + if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) { + g_save_modal_open = false; + } + ImGui::SameLine(); + if (fn_ui::button("Save", fn_ui::ButtonVariant::Primary)) { + g_save_err = save_current_as_generator(); + if (g_save_err.empty()) { + g_save_modal_open = false; + } + } + } + fn_ui::modal_dialog_end(); + + ImVec2 avail = ImGui::GetContentRegionAvail(); + float footer_h = g_code_err.empty() ? 0.0f : ImGui::GetTextLineHeightWithSpacing() + 8.0f; + ImVec2 editor_size(avail.x, avail.y - footer_h); + + char buf[1 << 16]; + size_t copy_len = g_source.size() < sizeof(buf) - 1 ? g_source.size() : sizeof(buf) - 1; + memcpy(buf, g_source.c_str(), copy_len); + buf[copy_len] = '\0'; + + if (ImGui::InputTextMultiline("##code", buf, sizeof(buf), editor_size, + ImGuiInputTextFlags_AllowTabInput)) { + g_source = buf; + mark_code_dirty(); + } + + draw_err(g_code_err, g_code_err_line); + } + ImGui::End(); + } + + // --- DAG Pipeline window --- + if (g_show_dag) { + if (ImGui::Begin("DAG Pipeline", &g_show_dag)) { + if (fn::gfx::dag_node_editor(g_pipeline)) { + g_dag_dirty = true; + } + draw_err(g_dag_err, g_dag_err_line); + } + ImGui::End(); + } + + // --- Canvas Code --- + // Code is fully independent from the DAG: only the uniforms declared in + // the Code source itself (parsed via parse_uniforms) get fed. To reproduce + // a DAG render here, paste the *baked* "Generated GLSL" — its u_params live + // as a const array inside the source. + if (g_show_canvas_c) { + if (ImGui::Begin("Canvas Code", &g_show_canvas_c)) { + fn::gfx::canvas_render(g_canvas_code, static_cast(ImGui::GetTime()), + [](unsigned int program) { + fn::gfx::uniforms_apply(g_store, g_descs, program); + }); + } + ImGui::End(); + } + + // --- Canvas DAG --- + if (g_show_canvas_d) { + if (ImGui::Begin("Canvas DAG", &g_show_canvas_d)) { + fn::gfx::canvas_render(g_canvas_dag, static_cast(ImGui::GetTime()), + [](unsigned int program) { + fn::gfx::dag_uniforms_apply(g_pipeline, program); + }); + } + ImGui::End(); + } + + if (g_canvas_dag.program) { + fn::gfx::dag_previews_render(g_pipeline, g_canvas_dag.program); + } + + // --- Controls window (Code uniforms) --- + if (g_show_controls) { + if (ImGui::Begin("Controls", &g_show_controls)) { + if (g_descs.empty()) { + ImGui::TextDisabled("No uniforms declared in Code."); + ImGui::TextDisabled("Use // @slider, @color, @toggle, @xy annotations."); + } else { + fn::gfx::uniforms_panel(g_store, g_descs); + } + // fps_overlay ahora se renderiza desde fn::run_app cuando el usuario + // lo activa en Settings → Show FPS overlay. + } + ImGui::End(); + } + + // --- Functions palette (drag into DAG Pipeline) --- + if (g_show_functions) { + if (ImGui::Begin("Functions", &g_show_functions)) { + fn::gfx::dag_palette(); + } + ImGui::End(); + } + + // --- Generated GLSL window (self-contained DAG → paste-able into Code) --- + // We bake the live params into a `const vec4 u_params[]` so the displayed + // text is a complete shader: copy-pasting it into the Code editor yields + // the same render at the moment of the copy, and nothing in the DAG can + // change the Code canvas afterwards. + if (g_show_generated) { + if (ImGui::Begin("Generated GLSL", &g_show_generated)) { + if (g_pipeline.empty()) { + ImGui::TextDisabled("(DAG not compiled yet)"); + } else { + static std::string s_baked; + s_baked = fn::gfx::compile_dag_to_glsl_baked(g_pipeline); + ImVec2 avail = ImGui::GetContentRegionAvail(); + ImGui::InputTextMultiline("##dag_glsl", + const_cast(s_baked.c_str()), + s_baked.size() + 1, + avail, + ImGuiInputTextFlags_ReadOnly); + } + } + ImGui::End(); + } +} + +int main() { + fn::gfx::shaderlab_db_open("shaders_lab.db"); + load_user_generators_into_catalog(); + ensure_dag_default(); + + // Layout persistence: handle opaco que crea su propia tabla + // imgui_layouts en shaders_lab.db (CREATE IF NOT EXISTS, no toca la + // tabla ui_layouts heredada). Cualquier app del registry puede usar + // este patron. + g_layouts = fn_ui::layout_storage_open("shaders_lab.db"); + fn_ui::layout_storage_make_callbacks(g_layouts, g_layout_cb); + + // Override de on_reset: ademas de limpiar el INI, re-mostrar todos + // los paneles especificos de shaders_lab. + g_layout_cb.on_reset = []() { + g_show_code = g_show_dag = g_show_canvas_c = g_show_canvas_d = + g_show_controls = g_show_functions = g_show_generated = true; + ImGui::LoadIniSettingsFromMemory("", 0); + ImGui::GetIO().WantSaveIniSettings = true; + g_layout_cb.active_name.clear(); + }; + + fn::AppConfig cfg; + cfg.title = "shaders_lab"; + cfg.width = 1600; + cfg.height = 900; + cfg.about = {.name = "shaders_lab", + .version = "0.3.0", + .description = "Live GLSL shader playground with DAG pipeline. layout_storage publico, compiler extraido, AppConfig estandar, multi-viewport, modal save-as via modal_dialog."}; + cfg.panels = k_panels; + cfg.panel_count = sizeof(k_panels) / sizeof(k_panels[0]); + cfg.layouts_cb = &g_layout_cb; + int rc = fn::run_app(cfg, render); + + fn::gfx::canvas_destroy(g_canvas_code); + fn::gfx::canvas_destroy(g_canvas_dag); + fn::gfx::dag_node_editor_destroy(); + fn::gfx::dag_previews_destroy(); + fn_ui::layout_storage_close(g_layouts); + fn::gfx::shaderlab_db_close(); + return rc; +}