From ab3115ce99ec68d3e611fe7e5b9104de0137e28a Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 25 Apr 2026 02:06:50 +0200 Subject: [PATCH] feat(shaders_lab): per-node preview thumbnails + double-rclick delete node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DagStep: preview_open flag (default false). - dag_compile: emit `uniform int u_preview_target` and a series of early-return branches at the start of fragColor selection. -1 (default) falls through to the real Output-driven fragColor. - dag_node_previews (new fn): per-node FBO keyed by editor_uid, lazy created. Renders each node with preview_open=true to its FBO by setting u_preview_target = step index. Texture exposed via dag_preview_texture(uid) for ImGui::Image. - dag_node_editor: small toggle button "[+] preview"/"[-] preview" in each non-Output node; when open, ImGui::Image(96x64, V-flipped). - dag_node_editor: double right-click on hovered node deletes it (Output is protected). - main.cpp: dag_previews_render after Canvas DAG; dag_previews_destroy on shutdown. Single GL program drives both the canvas and all thumbnails — moving sliders never recompiles, only the topology change does. Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/apps/shaders_lab/CMakeLists.txt | 1 + cpp/apps/shaders_lab/main.cpp | 7 ++ cpp/functions/gfx/dag_compile.cpp | 13 ++- cpp/functions/gfx/dag_node_editor.cpp | 42 ++++++++++ cpp/functions/gfx/dag_node_previews.cpp | 102 ++++++++++++++++++++++++ cpp/functions/gfx/dag_node_previews.h | 25 ++++++ cpp/functions/gfx/dag_node_previews.md | 55 +++++++++++++ cpp/functions/gfx/dag_types.h | 1 + 8 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 cpp/functions/gfx/dag_node_previews.cpp create mode 100644 cpp/functions/gfx/dag_node_previews.h create mode 100644 cpp/functions/gfx/dag_node_previews.md diff --git a/cpp/apps/shaders_lab/CMakeLists.txt b/cpp/apps/shaders_lab/CMakeLists.txt index 25b869e5..0923a461 100644 --- a/cpp/apps/shaders_lab/CMakeLists.txt +++ b/cpp/apps/shaders_lab/CMakeLists.txt @@ -13,6 +13,7 @@ add_imgui_app(shaders_lab ${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/core/fps_overlay.cpp ) target_include_directories(shaders_lab PRIVATE diff --git a/cpp/apps/shaders_lab/main.cpp b/cpp/apps/shaders_lab/main.cpp index c3647aa4..84dea2bd 100644 --- a/cpp/apps/shaders_lab/main.cpp +++ b/cpp/apps/shaders_lab/main.cpp @@ -11,6 +11,7 @@ #include "gfx/dag_panel.h" #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" @@ -187,6 +188,11 @@ static void render() { } 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_descs.empty()) { @@ -232,5 +238,6 @@ int main() { 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(); return rc; } diff --git a/cpp/functions/gfx/dag_compile.cpp b/cpp/functions/gfx/dag_compile.cpp index 09ecf122..2ff516c6 100644 --- a/cpp/functions/gfx/dag_compile.cpp +++ b/cpp/functions/gfx/dag_compile.cpp @@ -11,7 +11,8 @@ std::string compile_dag_to_glsl(const std::vector& pipeline) { const int n = static_cast(std::min(pipeline.size(), static_cast(MAX_NODES))); std::ostringstream out; - out << "uniform vec4 u_params[16];\n\n"; + out << "uniform vec4 u_params[16];\n"; + out << "uniform int u_preview_target; // -1 = real Output; >=0 = show out_\n\n"; if (n == 0) { out << "void main() {\n"; @@ -81,6 +82,16 @@ std::string compile_dag_to_glsl(const std::vector& pipeline) { last_valid_out = i; } + // Preview branch: if u_preview_target points to a valid out_, emit it + // and bail out before the Output-driven fragColor. + for (int i = 0; i < n; ++i) { + const DagStep& step = pipeline[static_cast(i)]; + const DagNodeDef* def = dag_find(step.name); + if (!def) continue; + if (def->kind == DagKind::Output) continue; + out << " if (u_preview_target == " << i << ") { fragColor = out_" << i << "; return; }\n"; + } + // 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"; }; diff --git a/cpp/functions/gfx/dag_node_editor.cpp b/cpp/functions/gfx/dag_node_editor.cpp index a90eb33d..1e2ef539 100644 --- a/cpp/functions/gfx/dag_node_editor.cpp +++ b/cpp/functions/gfx/dag_node_editor.cpp @@ -1,5 +1,6 @@ #include "gfx/dag_node_editor.h" #include "gfx/dag_catalog.h" +#include "gfx/dag_node_previews.h" #include "imgui.h" #include "imgui_node_editor.h" #include @@ -304,6 +305,25 @@ bool dag_node_editor(std::vector& pipeline) { } } } + + // Per-node preview thumbnail (off by default). + if (def->kind != DagKind::Output) { + char btn_lbl[64]; + std::snprintf(btn_lbl, sizeof(btn_lbl), "%s preview##pv%u", + step.preview_open ? "[-]" : "[+]", step.editor_uid); + if (ImGui::SmallButton(btn_lbl)) { + step.preview_open = !step.preview_open; + } + if (step.preview_open) { + unsigned tex = dag_preview_texture(step.editor_uid); + if (tex != 0) { + ImGui::Image(static_cast(static_cast(tex)), + ImVec2(96, 64), ImVec2(0, 1), ImVec2(1, 0)); + } else { + ImGui::Dummy(ImVec2(96, 64)); + } + } + } ImGui::PopID(); ImGui::EndGroup(); @@ -360,6 +380,28 @@ bool dag_node_editor(std::vector& pipeline) { ed::Resume(); } + // ── Double right-click on a node deletes it (Output is protected) ────── + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Right)) { + ed::NodeId hovered = ed::GetHoveredNode(); + uint32_t uid = static_cast(hovered.Get()); + if (uid != 0) { + int idx = find_by_uid(pipeline, uid); + if (idx >= 0) { + const DagNodeDef* d = dag_find(pipeline[static_cast(idx)].name); + if (d && d->kind != DagKind::Output) { + const std::string del_id = pipeline[static_cast(idx)].id; + for (auto& s : pipeline) { + for (auto& sid : s.source_ids) { + if (sid == del_id) sid.clear(); + } + } + pipeline.erase(pipeline.begin() + idx); + changed = true; + } + } + } + } + // ── Right-click on a pin clears all connections of that pin ──────────── { ed::Suspend(); diff --git a/cpp/functions/gfx/dag_node_previews.cpp b/cpp/functions/gfx/dag_node_previews.cpp new file mode 100644 index 00000000..e43a8825 --- /dev/null +++ b/cpp/functions/gfx/dag_node_previews.cpp @@ -0,0 +1,102 @@ +#include "gfx/gl_loader.h" +#include "gfx/dag_node_previews.h" +#include "gfx/dag_catalog.h" +#include "gfx/gl_framebuffer.h" +#include "gfx/fullscreen_quad.h" + +#include + +namespace fn::gfx { + +struct PreviewSlot { + Framebuffer fb; + bool inited = false; +}; + +static std::unordered_map s_slots; +static Quad s_quad; +static bool s_quad_init = false; + +static PreviewSlot& slot_for(unsigned uid) { + auto& slot = s_slots[uid]; + if (!slot.inited) { + fb_init(slot.fb); + slot.inited = true; + } + return slot; +} + +void dag_previews_render(const std::vector& pipeline, + unsigned program, + int width, int height) { + if (!program || pipeline.empty()) return; + + if (!s_quad_init) { + quad_init(s_quad); + s_quad_init = true; + } + + // Save GL state we'll touch + GLint prev_fbo = 0; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo); + GLint prev_vp[4]; + glGetIntegerv(GL_VIEWPORT, prev_vp); + GLint prev_program = 0; + glGetIntegerv(GL_CURRENT_PROGRAM, &prev_program); + + glUseProgram(program); + GLint loc_target = glGetUniformLocation(program, "u_preview_target"); + GLint loc_resolution = glGetUniformLocation(program, "u_resolution"); + GLint loc_time = glGetUniformLocation(program, "u_time"); + + if (loc_resolution >= 0) { + glUniform2f(loc_resolution, static_cast(width), static_cast(height)); + } + // u_time and u_params are already set by the main canvas pass for this frame. + + for (int i = 0; i < static_cast(pipeline.size()); ++i) { + const DagStep& step = pipeline[static_cast(i)]; + if (!step.preview_open) continue; + const DagNodeDef* def = dag_find(step.name); + if (!def || def->kind == DagKind::Output) continue; + + PreviewSlot& slot = slot_for(step.editor_uid); + fb_resize(slot.fb, width, height); + + glBindFramebuffer(GL_FRAMEBUFFER, slot.fb.fbo); + glViewport(0, 0, width, height); + + if (loc_target >= 0) glUniform1i(loc_target, i); + if (loc_resolution >= 0) glUniform2f(loc_resolution, static_cast(width), static_cast(height)); + + quad_draw(s_quad); + } + + // Reset preview target so the main canvas pass renders the real Output + if (loc_target >= 0) glUniform1i(loc_target, -1); + + // Restore state + glUseProgram(static_cast(prev_program)); + glBindFramebuffer(GL_FRAMEBUFFER, static_cast(prev_fbo)); + glViewport(prev_vp[0], prev_vp[1], prev_vp[2], prev_vp[3]); + if (loc_time >= 0) {} // silence unused +} + +unsigned dag_preview_texture(unsigned editor_uid) { + auto it = s_slots.find(editor_uid); + if (it == s_slots.end()) return 0; + return it->second.fb.tex; +} + +void dag_previews_destroy() { + for (auto& kv : s_slots) { + fb_destroy(kv.second.fb); + } + s_slots.clear(); + if (s_quad_init) { + quad_destroy(s_quad); + s_quad_init = false; + } +} + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/dag_node_previews.h b/cpp/functions/gfx/dag_node_previews.h new file mode 100644 index 00000000..94f2a900 --- /dev/null +++ b/cpp/functions/gfx/dag_node_previews.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include "gfx/dag_types.h" + +namespace fn::gfx { + +// Renders a small thumbnail per node that has preview_open == true, by binding +// the node's FBO and drawing the DAG shader with u_preview_target = step index. +// Each node's FBO is created lazily and keyed by editor_uid. The active GL +// program must be the one returned by compile_dag_to_glsl(pipeline). +// +// Pass the pipeline used to compile `program` so target indices match. +void dag_previews_render(const std::vector& pipeline, + unsigned program, + int width = 96, int height = 64); + +// Returns the OpenGL texture id for the preview of `editor_uid`, or 0 if +// none exists. Use with ImGui::Image(). Call AFTER dag_previews_render so +// the texture has fresh contents. +unsigned dag_preview_texture(unsigned editor_uid); + +// Free all preview FBOs. Call on shutdown. +void dag_previews_destroy(); + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/dag_node_previews.md b/cpp/functions/gfx/dag_node_previews.md new file mode 100644 index 00000000..7ac7d6bc --- /dev/null +++ b/cpp/functions/gfx/dag_node_previews.md @@ -0,0 +1,55 @@ +--- +name: dag_node_previews +kind: function +lang: cpp +domain: gfx +version: "1.0.0" +purity: impure +signature: "void dag_previews_render(const std::vector&, unsigned program, int w, int h); unsigned dag_preview_texture(unsigned editor_uid); void dag_previews_destroy()" +description: "Renderiza un thumbnail por cada DagStep con preview_open=true a un FBO propio (lazy, keyed por editor_uid). Bind del FBO + glUniform1i(u_preview_target, i) + draw del quad. Devuelve la textura via dag_preview_texture para mostrarla con ImGui::Image." +tags: [opengl, fbo, preview, dag, gfx] +uses_functions: [gl_loader_cpp_gfx, gl_framebuffer_cpp_gfx, fullscreen_quad_cpp_gfx, dag_catalog_cpp_gfx] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [gl_loader, dag_catalog, gl_framebuffer, fullscreen_quad] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/gfx/dag_node_previews.cpp" +framework: opengl +params: + - name: pipeline + desc: "Pipeline DAG. Solo nodos con preview_open=true generan thumbnail. Output nodes se ignoran." + - name: program + desc: "GL program compilado a partir de compile_dag_to_glsl(pipeline). Debe declarar u_preview_target." + - name: width + desc: "Ancho del thumbnail en pixeles. Default 96." + - name: height + desc: "Alto del thumbnail en pixeles. Default 64." +output: "Cada FBO contiene out_ renderizado a width x height. Acceso via dag_preview_texture(uid)." +--- + +# dag_node_previews + +Pipeline para thumbnails por nodo. Reutiliza el program GL del DAG (con un branch al final controlado por u_preview_target) y renderiza N veces a FBOs pequeños keyed por editor_uid del nodo. + +No recompila al mover sliders ni al togglear preview — solo cambia el u_preview_target. + +## Uso + +```cpp +fn::gfx::canvas_render(g_canvas_dag, time, [&](unsigned p) { + fn::gfx::dag_uniforms_apply(g_pipeline, p); +}); +fn::gfx::dag_previews_render(g_pipeline, g_canvas_dag.program); + +// Luego en el editor del nodo: +unsigned tex = fn::gfx::dag_preview_texture(step.editor_uid); +if (tex) ImGui::Image((ImTextureID)(intptr_t)tex, ImVec2(96, 64)); +``` + +## Recursos + +Cada FBO se mantiene en un map estatico hasta llamar `dag_previews_destroy()` en shutdown. No se libera al borrar un nodo (pequena fuga en sesion larga; aceptable para 16 nodos max). diff --git a/cpp/functions/gfx/dag_types.h b/cpp/functions/gfx/dag_types.h index 100fcf48..06e3621d 100644 --- a/cpp/functions/gfx/dag_types.h +++ b/cpp/functions/gfx/dag_types.h @@ -38,6 +38,7 @@ struct DagStep { 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_ }; } // namespace fn::gfx