feat(shaders_lab): per-node preview thumbnails + double-rclick delete node

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 02:06:50 +02:00
parent a209afa46b
commit ac65791663
9 changed files with 245 additions and 1 deletions
Binary file not shown.
+1
View File
@@ -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
+7
View File
@@ -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;
}
+12 -1
View File
@@ -11,7 +11,8 @@ 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\n";
out << "uniform vec4 u_params[16];\n";
out << "uniform int u_preview_target; // -1 = real Output; >=0 = show out_<i>\n\n";
if (n == 0) {
out << "void main() {\n";
@@ -81,6 +82,16 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
last_valid_out = i;
}
// Preview branch: if u_preview_target points to a valid out_<i>, emit it
// and bail out before the Output-driven fragColor.
for (int i = 0; i < n; ++i) {
const DagStep& step = pipeline[static_cast<size_t>(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"; };
+42
View File
@@ -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 <algorithm>
@@ -304,6 +305,25 @@ bool dag_node_editor(std::vector<DagStep>& 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<ImTextureID>(static_cast<intptr_t>(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<DagStep>& 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<uint32_t>(hovered.Get());
if (uid != 0) {
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->kind != DagKind::Output) {
const std::string del_id = pipeline[static_cast<size_t>(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();
+102
View File
@@ -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 <unordered_map>
namespace fn::gfx {
struct PreviewSlot {
Framebuffer fb;
bool inited = false;
};
static std::unordered_map<unsigned, PreviewSlot> 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<DagStep>& 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<float>(width), static_cast<float>(height));
}
// u_time and u_params are already set by the main canvas pass for this frame.
for (int i = 0; i < static_cast<int>(pipeline.size()); ++i) {
const DagStep& step = pipeline[static_cast<size_t>(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<float>(width), static_cast<float>(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<GLuint>(prev_program));
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(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
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <vector>
#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<DagStep>& 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
+55
View File
@@ -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<DagStep>&, 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_<i> 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).
+1
View File
@@ -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_<index>
};
} // namespace fn::gfx