diff --git a/cpp/apps/shaders_lab/CMakeLists.txt b/cpp/apps/shaders_lab/CMakeLists.txt index 02716e20..25b869e5 100644 --- a/cpp/apps/shaders_lab/CMakeLists.txt +++ b/cpp/apps/shaders_lab/CMakeLists.txt @@ -12,6 +12,7 @@ add_imgui_app(shaders_lab ${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/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 7b9e6227..c3647aa4 100644 --- a/cpp/apps/shaders_lab/main.cpp +++ b/cpp/apps/shaders_lab/main.cpp @@ -10,6 +10,7 @@ #include "gfx/dag_uniforms.h" #include "gfx/dag_panel.h" #include "gfx/dag_node_editor.h" +#include "gfx/dag_palette.h" #include "core/fps_overlay.h" #include "seed_shaders.h" @@ -73,14 +74,32 @@ static void load_preset(const char* src) { } static void ensure_dag_default() { + // Seed with a Plasma connected to an Output node. if (g_pipeline.empty()) { - const fn::gfx::DagNodeDef* def = fn::gfx::dag_find("plasma"); - if (def) { - fn::gfx::DagStep step; - step.id = "n_default"; - step.name = def->name; - step.params = def->param_defaults; - g_pipeline.push_back(step); + 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); + } + } + // 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); + 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); } } } @@ -181,6 +200,12 @@ static void render() { } ImGui::End(); + // --- Functions palette (drag into DAG Pipeline) --- + if (ImGui::Begin("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()) { diff --git a/cpp/functions/gfx/dag_catalog.cpp b/cpp/functions/gfx/dag_catalog.cpp index dafd507d..d687ae66 100644 --- a/cpp/functions/gfx/dag_catalog.cpp +++ b/cpp/functions/gfx/dag_catalog.cpp @@ -225,6 +225,21 @@ static const std::vector& build_catalog() { v.push_back(std::move(n)); } + // ── Output (sink — drives fragColor) ───────────────────────── + { + DagNodeDef n; + n.name = "output"; + n.label = "Output"; + 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.controls = {}; + n.body_glsl = [](int) -> std::string { return ""; }; + v.push_back(std::move(n)); + } + return v; }(); return catalog; diff --git a/cpp/functions/gfx/dag_compile.cpp b/cpp/functions/gfx/dag_compile.cpp index 4e8a64a2..591d7189 100644 --- a/cpp/functions/gfx/dag_compile.cpp +++ b/cpp/functions/gfx/dag_compile.cpp @@ -22,11 +22,12 @@ std::string compile_dag_to_glsl(const std::vector& pipeline) { return out.str(); } - // Emit per-node functions with signatures based on num_inputs + // 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(i)]; const DagNodeDef* def = dag_find(step.name); if (!def) continue; + if (def->kind == DagKind::Output) continue; int ni = def->num_inputs; out << "vec4 node_" << i << "("; @@ -43,34 +44,30 @@ std::string compile_dag_to_glsl(const std::vector& pipeline) { out << "void main() {\n"; out << " vec2 uv = gl_FragCoord.xy / u_resolution;\n"; + int last_valid_out = -1; + int output_idx = -1; + for (int i = 0; i < n; ++i) { const DagStep& step = pipeline[static_cast(i)]; const DagNodeDef* def = dag_find(step.name); - if (!def) { - if (i == 0) { - out << " vec4 out_" << i << " = vec4(0.0, 0.0, 0.0, 1.0);\n"; - } else { - out << " vec4 out_" << i << " = out_" << (i - 1) << ";\n"; - } - continue; - } + if (!def) continue; + if (def->kind == DagKind::Output) { output_idx = i; continue; } int ni = def->num_inputs; - // Resolve each input slot - // For slot k: look for source_ids[k] in pipeline[0..i-1]; fallback = prev output auto resolve = [&](int k) -> std::string { const std::string& sid = step.source_ids[static_cast(k)]; if (!sid.empty()) { for (int j = 0; j < i; ++j) { if (pipeline[static_cast(j)].id == sid) { + const DagNodeDef* jdef = dag_find(pipeline[static_cast(j)].name); + if (jdef && jdef->kind == DagKind::Output) continue; // Output has no out_j return "out_" + std::to_string(j); } } } - // fallback - if (i == 0) return "vec4(0.0, 0.0, 0.0, 1.0)"; - return "out_" + std::to_string(i - 1); + if (last_valid_out < 0) return "vec4(0.0, 0.0, 0.0, 1.0)"; + return "out_" + std::to_string(last_valid_out); }; out << " vec4 out_" << i << " = node_" << i << "("; @@ -80,9 +77,29 @@ std::string compile_dag_to_glsl(const std::vector& pipeline) { } if (ni > 0) out << ", "; out << "uv);\n"; + + last_valid_out = i; + } + + // 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"; }; + + if (output_idx >= 0) { + const std::string& sid = pipeline[static_cast(output_idx)].source_ids[0]; + int src = -1; + if (!sid.empty()) { + for (int j = 0; j < output_idx; ++j) { + if (pipeline[static_cast(j)].id == sid) { src = j; break; } + } + } + 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(); } - out << " fragColor = out_" << (n - 1) << ";\n"; out << "}\n"; return out.str(); diff --git a/cpp/functions/gfx/dag_node_editor.cpp b/cpp/functions/gfx/dag_node_editor.cpp index c57df7d7..4419bbc4 100644 --- a/cpp/functions/gfx/dag_node_editor.cpp +++ b/cpp/functions/gfx/dag_node_editor.cpp @@ -61,9 +61,10 @@ static int find_by_id(const std::vector& p, const std::string& id) { static ImVec4 kind_color(DagKind kind) { switch (kind) { - case DagKind::Gen: return ImVec4(0.25f, 0.55f, 0.90f, 1.0f); - case DagKind::Op: return ImVec4(0.65f, 0.40f, 0.90f, 1.0f); - case DagKind::Blend: return ImVec4(0.90f, 0.65f, 0.15f, 1.0f); + case DagKind::Gen: return ImVec4(0.25f, 0.55f, 0.90f, 1.0f); + case DagKind::Op: return ImVec4(0.65f, 0.40f, 0.90f, 1.0f); + case DagKind::Blend: return ImVec4(0.90f, 0.65f, 0.15f, 1.0f); + case DagKind::Output: return ImVec4(0.85f, 0.25f, 0.25f, 1.0f); } return ImVec4(1, 1, 1, 1); } @@ -120,70 +121,6 @@ static bool topo_sort(std::vector& pipeline) { return true; } -// Add node popup toolbar — rendered OUTSIDE ed::Begin -static bool draw_add_toolbar(std::vector& pipeline) { - bool changed = false; - int sz = static_cast(pipeline.size()); - if (ImGui::Button("+ Add Node")) { - ImGui::OpenPopup("ne_add_popup"); - } - ImGui::SameLine(); - ImGui::Text("%d/%d", sz, MAX_NODES); - ImGui::SameLine(); - if (ImGui::Button("Clear") && !pipeline.empty()) { - ImGui::OpenPopup("ne_clear_confirm"); - } - ImGui::SameLine(); - if (ImGui::Button("Fit")) { - if (s_ctx) { - ed::SetCurrentEditor(s_ctx); - ed::NavigateToContent(0.0f); - ed::SetCurrentEditor(nullptr); - } - } - - if (ImGui::BeginPopupModal("ne_clear_confirm", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("Vaciar el pipeline?"); - if (ImGui::Button("Si")) { - pipeline.clear(); - changed = true; - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("No")) { ImGui::CloseCurrentPopup(); } - ImGui::EndPopup(); - } - - if (ImGui::BeginPopup("ne_add_popup")) { - const char* kind_names[] = { "Gen", "Op", "Blend" }; - DagKind kinds[] = { DagKind::Gen, DagKind::Op, DagKind::Blend }; - for (int k = 0; k < 3; ++k) { - if (ImGui::BeginMenu(kind_names[k])) { - for (const auto& def : dag_catalog()) { - if (def.kind != kinds[k]) continue; - if (ImGui::MenuItem(def.label.c_str())) { - if (sz < MAX_NODES) { - DagStep step; - step.id = "n" + std::to_string(s_next_uid); - step.name = def.name; - step.params = def.param_defaults; - step.editor_uid = s_next_uid++; - // stagger position so nodes don't stack - step.editor_pos_x = 50.0f + static_cast(sz) * 220.0f; - step.editor_pos_y = 100.0f; - pipeline.push_back(step); - changed = true; - } - } - } - ImGui::EndMenu(); - } - } - ImGui::EndPopup(); - } - return changed; -} - bool dag_node_editor(std::vector& pipeline) { bool changed = false; @@ -201,12 +138,47 @@ bool dag_node_editor(std::vector& pipeline) { s_ctx = ed::CreateEditor(&cfg); } - changed |= draw_add_toolbar(pipeline); - ImGui::Separator(); + // Palette drop zone: accept dropped node types (from the Functions panel) + // and queue an add at the mouse canvas position (resolved after ed::Begin). + static std::string s_pending_add_name; + static ImVec2 s_pending_add_pos(0, 0); + static bool s_pending_add = false; + + ImVec2 origin = ImGui::GetCursorScreenPos(); + ImVec2 avail = ImGui::GetContentRegionAvail(); + ImGui::InvisibleButton("##dag_drop_zone", avail, + ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight); + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* p = ImGui::AcceptDragDropPayload("DAG_NODE_TYPE")) { + s_pending_add_name.assign(static_cast(p->Data), static_cast(p->DataSize)); + s_pending_add_pos = ImGui::GetMousePos(); + s_pending_add = true; + } + ImGui::EndDragDropTarget(); + } + ImGui::SetCursorScreenPos(origin); ed::SetCurrentEditor(s_ctx); ed::Begin("dag_editor", ImVec2(0.0f, 0.0f)); + if (s_pending_add) { + const DagNodeDef* def = dag_find(s_pending_add_name); + if (def && static_cast(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; + pipeline.push_back(step); + changed = true; + } + s_pending_add = false; + } + // ── Draw nodes ─────────────────────────────────────────────────────────── for (int i = 0; i < static_cast(pipeline.size()); ++i) { DagStep& step = pipeline[static_cast(i)]; @@ -279,11 +251,13 @@ bool dag_node_editor(std::vector& pipeline) { } ImGui::PopID(); - // Output pin - ImGui::Dummy(ImVec2(0, 2)); - ed::BeginPin(ed::PinId(output_pin_id(step.editor_uid)), ed::PinKind::Output); - ImGui::Text("out ->"); - ed::EndPin(); + // Output pin (skip for the terminal Output node — it has no output) + if (def->kind != DagKind::Output) { + ImGui::Dummy(ImVec2(0, 2)); + ed::BeginPin(ed::PinId(output_pin_id(step.editor_uid)), ed::PinKind::Output); + ImGui::Text("out ->"); + ed::EndPin(); + } ed::EndNode(); } @@ -369,12 +343,17 @@ bool dag_node_editor(std::vector& pipeline) { ed::NodeId del_nid; while (ed::QueryDeletedNode(&del_nid)) { + uint32_t uid = static_cast(del_nid.Get()); + int idx = find_by_uid(pipeline, uid); + // Refuse to delete the Output node (it's the sink) + const DagNodeDef* ddef = (idx >= 0) ? dag_find(pipeline[static_cast(idx)].name) : nullptr; + if (ddef && ddef->kind == DagKind::Output) { + ed::RejectDeletedItem(); + continue; + } if (ed::AcceptDeletedItem()) { - uint32_t uid = static_cast(del_nid.Get()); - int idx = find_by_uid(pipeline, uid); if (idx >= 0) { const std::string& del_step_id = pipeline[static_cast(idx)].id; - // Clear any source_ids pointing to the deleted node for (auto& step : pipeline) { for (auto& sid : step.source_ids) { if (sid == del_step_id) sid.clear(); diff --git a/cpp/functions/gfx/dag_palette.cpp b/cpp/functions/gfx/dag_palette.cpp new file mode 100644 index 00000000..2e401a22 --- /dev/null +++ b/cpp/functions/gfx/dag_palette.cpp @@ -0,0 +1,54 @@ +#include "gfx/dag_palette.h" +#include "gfx/dag_catalog.h" +#include "imgui.h" + +namespace fn::gfx { + +static ImVec4 kind_tint(DagKind kind) { + switch (kind) { + case DagKind::Gen: return ImVec4(0.25f, 0.55f, 0.90f, 1.0f); + case DagKind::Op: return ImVec4(0.65f, 0.40f, 0.90f, 1.0f); + case DagKind::Blend: return ImVec4(0.90f, 0.65f, 0.15f, 1.0f); + case DagKind::Output: return ImVec4(0.85f, 0.25f, 0.25f, 1.0f); + } + return ImVec4(1, 1, 1, 1); +} + +static void draw_group(const char* header, DagKind kind) { + if (!ImGui::CollapsingHeader(header, ImGuiTreeNodeFlags_DefaultOpen)) return; + + ImVec4 col = kind_tint(kind); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(col.x * 0.35f, col.y * 0.35f, col.z * 0.35f, 0.9f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(col.x * 0.55f, col.y * 0.55f, col.z * 0.55f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(col.x * 0.75f, col.y * 0.75f, col.z * 0.75f, 1.0f)); + + for (const auto& def : dag_catalog()) { + if (def.kind != kind) continue; + ImGui::PushID(def.name.c_str()); + ImGui::Button(def.label.c_str(), ImVec2(-1, 0)); + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) { + ImGui::SetDragDropPayload("DAG_NODE_TYPE", def.name.c_str(), def.name.size()); + ImGui::Text("+ %s", def.label.c_str()); + if (!def.desc.empty()) { + ImGui::TextDisabled("%s", def.desc.c_str()); + } + ImGui::EndDragDropSource(); + } + if (ImGui::IsItemHovered() && !def.desc.empty()) { + ImGui::SetTooltip("%s", def.desc.c_str()); + } + ImGui::PopID(); + } + + ImGui::PopStyleColor(3); +} + +void dag_palette() { + ImGui::TextDisabled("Drag into the DAG Pipeline canvas."); + ImGui::Spacing(); + draw_group("Generators", DagKind::Gen); + draw_group("Operators", DagKind::Op); + draw_group("Blends", DagKind::Blend); +} + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/dag_palette.h b/cpp/functions/gfx/dag_palette.h new file mode 100644 index 00000000..e782a7de --- /dev/null +++ b/cpp/functions/gfx/dag_palette.h @@ -0,0 +1,11 @@ +#pragma once + +namespace fn::gfx { + +// Renderiza una paleta del catalogo de nodos agrupada por kind (Gen / Op / Blend). +// Cada item es un drag source con payload "DAG_NODE_TYPE" (nombre del nodo). +// El Output no aparece en la paleta (es un sink fijo en el grafo). +// Llamar dentro de un ImGui::Begin/End. +void dag_palette(); + +} // namespace fn::gfx diff --git a/cpp/functions/gfx/dag_palette.md b/cpp/functions/gfx/dag_palette.md new file mode 100644 index 00000000..7cba964e --- /dev/null +++ b/cpp/functions/gfx/dag_palette.md @@ -0,0 +1,43 @@ +--- +name: dag_palette +kind: component +lang: cpp +domain: gfx +version: "1.0.0" +purity: impure +signature: "void dag_palette()" +description: "Paleta de nodos del catalogo, agrupada por kind (Gen/Op/Blend). Cada entrada es un drag source con payload DAG_NODE_TYPE que contiene el name del nodo. El node editor recibe el drop y anade un nuevo DagStep en la posicion del mouse." +tags: [imgui, dag, palette, drag-and-drop, gfx, component] +uses_functions: [dag_catalog_cpp_gfx] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [imgui, dag_catalog] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/gfx/dag_palette.cpp" +framework: imgui +params: [] +output: "Dibuja los widgets de paleta dentro del ImGui::Begin/End del caller. No tiene estado propio." +--- + +# dag_palette + +Paleta draggable de nodos del catalogo. Sustituye el boton "+ Add Node" del panel del DAG: en lugar de pulsar y navegar un submenu, el usuario arrastra el nodo desde la paleta al canvas del editor. + +## Uso + +```cpp +if (ImGui::Begin("Functions")) { + fn::gfx::dag_palette(); +} +ImGui::End(); +``` + +El node editor debe aceptar payloads con identificador `"DAG_NODE_TYPE"` y crear el `DagStep` correspondiente en la posicion del cursor. + +## Drop zone del editor + +El `dag_node_editor` usa un `ImGui::InvisibleButton` del tamano del canvas como drop target antes de `ed::Begin()`, y resetea el cursor para que el editor se dibuje encima. Al detectar drop, convierte la posicion screen a canvas con `ed::ScreenToCanvas()` y encola un add que se aplica tras abrir el contexto del editor. diff --git a/cpp/functions/gfx/dag_panel.cpp b/cpp/functions/gfx/dag_panel.cpp index ad937ad4..2d4c5d11 100644 --- a/cpp/functions/gfx/dag_panel.cpp +++ b/cpp/functions/gfx/dag_panel.cpp @@ -16,18 +16,20 @@ static std::string make_id() { static ImVec4 kind_color(DagKind kind) { switch (kind) { - case DagKind::Gen: return ImVec4(0.25f, 0.55f, 0.90f, 1.0f); - case DagKind::Op: return ImVec4(0.65f, 0.40f, 0.90f, 1.0f); - case DagKind::Blend: return ImVec4(0.90f, 0.65f, 0.15f, 1.0f); + case DagKind::Gen: return ImVec4(0.25f, 0.55f, 0.90f, 1.0f); + case DagKind::Op: return ImVec4(0.65f, 0.40f, 0.90f, 1.0f); + case DagKind::Blend: return ImVec4(0.90f, 0.65f, 0.15f, 1.0f); + case DagKind::Output: return ImVec4(0.85f, 0.25f, 0.25f, 1.0f); } return ImVec4(1, 1, 1, 1); } static const char* kind_label(DagKind kind) { switch (kind) { - case DagKind::Gen: return "Gen"; - case DagKind::Op: return "Op"; - case DagKind::Blend: return "Blend"; + case DagKind::Gen: return "Gen"; + case DagKind::Op: return "Op"; + case DagKind::Blend: return "Blend"; + case DagKind::Output: return "Output"; } return "?"; } diff --git a/cpp/functions/gfx/dag_types.h b/cpp/functions/gfx/dag_types.h index cd2ae06d..100fcf48 100644 --- a/cpp/functions/gfx/dag_types.h +++ b/cpp/functions/gfx/dag_types.h @@ -6,7 +6,7 @@ namespace fn::gfx { -enum class DagKind { Gen, Op, Blend }; +enum class DagKind { Gen, Op, Blend, Output }; struct DagControl { enum class Kind { Slider, XY, Color };