feat(shaders_lab): Output node + Functions palette with drag-drop
- DagKind::Output (new enum): terminal sink; compiler wires fragColor to its source_ids[0] - dag_catalog: "output" node (1 input, red) - dag_compile: skips Output in node_<i> emission; final fragColor resolves from Output's connection - dag_node_editor: no more Add button; drops "DAG_NODE_TYPE" payloads at mouse canvas position; Output cannot be deleted; Output has no output pin - dag_palette (new fn): Functions window with grouped, draggable node cards - main.cpp: "Functions" window added; ensure_dag_default seeds plasma + connected Output
This commit is contained in:
@@ -225,6 +225,21 @@ static const std::vector<DagNodeDef>& 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;
|
||||
|
||||
@@ -22,11 +22,12 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& 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<size_t>(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<DagStep>& 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<size_t>(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<size_t>(k)];
|
||||
if (!sid.empty()) {
|
||||
for (int j = 0; j < i; ++j) {
|
||||
if (pipeline[static_cast<size_t>(j)].id == sid) {
|
||||
const DagNodeDef* jdef = dag_find(pipeline[static_cast<size_t>(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<DagStep>& 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<size_t>(output_idx)].source_ids[0];
|
||||
int src = -1;
|
||||
if (!sid.empty()) {
|
||||
for (int j = 0; j < output_idx; ++j) {
|
||||
if (pipeline[static_cast<size_t>(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();
|
||||
|
||||
@@ -61,9 +61,10 @@ static int find_by_id(const std::vector<DagStep>& 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<DagStep>& pipeline) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add node popup toolbar — rendered OUTSIDE ed::Begin
|
||||
static bool draw_add_toolbar(std::vector<DagStep>& pipeline) {
|
||||
bool changed = false;
|
||||
int sz = static_cast<int>(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<float>(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<DagStep>& pipeline) {
|
||||
bool changed = false;
|
||||
|
||||
@@ -201,12 +138,47 @@ bool dag_node_editor(std::vector<DagStep>& 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<const char*>(p->Data), static_cast<size_t>(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<int>(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<int>(pipeline.size()); ++i) {
|
||||
DagStep& step = pipeline[static_cast<size_t>(i)];
|
||||
@@ -279,11 +251,13 @@ bool dag_node_editor(std::vector<DagStep>& 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<DagStep>& pipeline) {
|
||||
|
||||
ed::NodeId del_nid;
|
||||
while (ed::QueryDeletedNode(&del_nid)) {
|
||||
uint32_t uid = static_cast<uint32_t>(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<size_t>(idx)].name) : nullptr;
|
||||
if (ddef && ddef->kind == DagKind::Output) {
|
||||
ed::RejectDeletedItem();
|
||||
continue;
|
||||
}
|
||||
if (ed::AcceptDeletedItem()) {
|
||||
uint32_t uid = static_cast<uint32_t>(del_nid.Get());
|
||||
int idx = find_by_uid(pipeline, uid);
|
||||
if (idx >= 0) {
|
||||
const std::string& del_step_id = pipeline[static_cast<size_t>(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();
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 "?";
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user