#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.1.0", .description = "Live GLSL shader playground with DAG pipeline"}; 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; }