feat(shaders_lab): visual node editor (imgui-node-editor) + multi-source

- cpp/vendor/imgui-node-editor: vendorized thedmd/imgui-node-editor v0.9.4
- cpp/functions/gfx/dag_node_editor: new visual pipeline editor replacing dag_panel
- DagStep: source_ids[4] + editor_pos + editor_uid (multi-input support)
- DagNodeDef: num_inputs explicit; circle reclassified as Op
- dag_compile: N inputs per node, topological ordering preserved
- main: use node editor; destroy on shutdown
- patch imgui_extra_math.inl: guard operator*(float, ImVec2) for imgui >= 18955

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 21:55:43 +02:00
parent bf5011de93
commit 88fca7b128
89 changed files with 22289 additions and 41 deletions
+20 -9
View File
@@ -14,6 +14,7 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.label = "solid";
n.desc = "color constante";
n.kind = DagKind::Gen;
n.num_inputs = 0;
n.param_names = {"r", "g", "b", ""};
n.param_defaults = {0.35f, 0.25f, 0.55f, 0.0f};
n.controls = {
@@ -34,6 +35,7 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.label = "gradient";
n.desc = "gradiente direccional";
n.kind = DagKind::Gen;
n.num_inputs = 0;
n.param_names = {"angle", "hue", "", ""};
n.param_defaults = {0.8f, 0.5f, 0.0f, 0.0f};
n.controls = {
@@ -58,6 +60,7 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.label = "plasma";
n.desc = "onda trigonometrica";
n.kind = DagKind::Gen;
n.num_inputs = 0;
n.param_names = {"speed", "scale", "", ""};
n.param_defaults = {1.0f, 2.0f, 0.0f, 0.0f};
n.controls = {
@@ -73,13 +76,15 @@ static const std::vector<DagNodeDef>& build_catalog() {
v.push_back(std::move(n));
}
// ── Gen: circle ───────────────────────────────────────────────
// ── Op: circle ───────────────────────────────────────────────
// Reclassified as Op (num_inputs=1): composites circle over input 'a'
{
DagNodeDef n;
n.name = "circle";
n.label = "circle";
n.desc = "sdf de circulo";
n.kind = DagKind::Gen;
n.desc = "sdf de circulo (composita sobre input)";
n.kind = DagKind::Op;
n.num_inputs = 1;
n.param_names = {"cx", "cy", "radius", "soft"};
n.param_defaults = {0.0f, 0.0f, 0.35f, 0.01f};
n.controls = {
@@ -94,7 +99,7 @@ static const std::vector<DagNodeDef>& build_catalog() {
" vec2 pos = vec2((uv.x - 0.5) * aspect - p.x, uv.y - 0.5 - p.y);\n"
" float d = length(pos) - p.z;\n"
" float fill = smoothstep(p.w, -p.w, d);\n"
" return mix(c, vec4(1.0), fill);";
" return mix(a, vec4(1.0), fill);";
};
v.push_back(std::move(n));
}
@@ -106,11 +111,12 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.label = "invert";
n.desc = "1 - rgb";
n.kind = DagKind::Op;
n.num_inputs = 1;
n.param_names = {"", "", "", ""};
n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f};
n.controls = {};
n.body_glsl = [](int /*idx*/) -> std::string {
return " return vec4(1.0 - c.rgb, c.a);";
return " return vec4(1.0 - a.rgb, a.a);";
};
v.push_back(std::move(n));
}
@@ -122,6 +128,7 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.label = "gamma";
n.desc = "pow(rgb, gamma)";
n.kind = DagKind::Op;
n.num_inputs = 1;
n.param_names = {"gamma", "", "", ""};
n.param_defaults = {1.0f, 0.0f, 0.0f, 0.0f};
n.controls = {
@@ -130,7 +137,7 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.body_glsl = [](int idx) -> std::string {
std::string i = std::to_string(idx);
return " vec4 p = u_params[" + i + "];\n"
" return vec4(pow(c.rgb, vec3(1.0 / max(p.x, 0.001))), c.a);";
" return vec4(pow(a.rgb, vec3(1.0 / max(p.x, 0.001))), a.a);";
};
v.push_back(std::move(n));
}
@@ -142,6 +149,7 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.label = "hue shift";
n.desc = "rotar matiz";
n.kind = DagKind::Op;
n.num_inputs = 1;
n.param_names = {"h", "", "", ""};
n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f};
n.controls = {
@@ -150,14 +158,14 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.body_glsl = [](int idx) -> std::string {
std::string i = std::to_string(idx);
return " vec4 p = u_params[" + i + "];\n"
" float a = 6.28318 * p.x;\n"
" float ca = cos(a), sa = sin(a);\n"
" float ang = 6.28318 * p.x;\n"
" float ca = cos(ang), sa = sin(ang);\n"
" mat3 hueMat = mat3(\n"
" vec3(0.299 + 0.701 * ca + 0.168 * sa, 0.587 - 0.587 * ca + 0.330 * sa, 0.114 - 0.114 * ca - 0.497 * sa),\n"
" vec3(0.299 - 0.299 * ca - 0.328 * sa, 0.587 + 0.413 * ca + 0.035 * sa, 0.114 - 0.114 * ca + 0.292 * sa),\n"
" vec3(0.299 - 0.300 * ca + 1.250 * sa, 0.587 - 0.588 * ca - 1.050 * sa, 0.114 + 0.886 * ca - 0.203 * sa)\n"
" );\n"
" return vec4(clamp(hueMat * c.rgb, 0.0, 1.0), c.a);";
" return vec4(clamp(hueMat * a.rgb, 0.0, 1.0), a.a);";
};
v.push_back(std::move(n));
}
@@ -169,6 +177,7 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.label = "mix";
n.desc = "interpolacion mix(a, b, t)";
n.kind = DagKind::Blend;
n.num_inputs = 2;
n.param_names = {"t", "", "", ""};
n.param_defaults = {0.5f, 0.0f, 0.0f, 0.0f};
n.controls = {
@@ -189,6 +198,7 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.label = "multiply";
n.desc = "a * b";
n.kind = DagKind::Blend;
n.num_inputs = 2;
n.param_names = {"", "", "", ""};
n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f};
n.controls = {};
@@ -205,6 +215,7 @@ static const std::vector<DagNodeDef>& build_catalog() {
n.label = "screen";
n.desc = "1 - (1-a)(1-b)";
n.kind = DagKind::Blend;
n.num_inputs = 2;
n.param_names = {"", "", "", ""};
n.param_defaults = {0.0f, 0.0f, 0.0f, 0.0f};
n.controls = {};
+33 -26
View File
@@ -22,57 +22,64 @@ std::string compile_dag_to_glsl(const std::vector<DagStep>& pipeline) {
return out.str();
}
// Emit per-node functions with signatures based on num_inputs
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::Blend) {
out << "vec4 node_" << i << "(vec4 a, vec4 b, vec2 uv) {\n";
} else {
out << "vec4 node_" << i << "(vec4 c, vec2 uv) {\n";
}
int ni = def->num_inputs;
out << "vec4 node_" << i << "(";
if (ni >= 1) out << "vec4 a";
if (ni >= 2) out << ", vec4 b";
if (ni >= 3) out << ", vec4 c";
if (ni >= 4) out << ", vec4 d";
if (ni > 0) out << ", ";
out << "vec2 uv) {\n";
out << def->body_glsl(i) << "\n";
out << "}\n\n";
}
out << "void main() {\n";
out << " vec2 uv = gl_FragCoord.xy / u_resolution;\n";
out << " vec4 c = vec4(0.04, 0.04, 0.06, 1.0);\n";
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) {
out << " vec4 out_" << i << " = (i > 0 ? out_" << (i-1) << " : c);\n";
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;
}
std::string prev = (i == 0) ? "vec4(0.0, 0.0, 0.0, 1.0)" : "out_" + std::to_string(i - 1);
int ni = def->num_inputs;
if (def->kind == DagKind::Blend) {
int src_idx = -1;
if (!step.source_id.empty()) {
// 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 == step.source_id) {
src_idx = j;
break;
if (pipeline[static_cast<size_t>(j)].id == sid) {
return "out_" + std::to_string(j);
}
}
}
if (src_idx < 0 || src_idx >= i) {
src_idx = std::max(0, i - 2);
}
std::string src;
if (i == 0) {
src = prev;
} else {
src = "out_" + std::to_string(std::min(src_idx, i - 1));
}
out << " vec4 out_" << i << " = node_" << i << "(" << prev << ", " << src << ", uv);\n";
} else {
out << " vec4 out_" << i << " = node_" << i << "(" << prev << ", uv);\n";
// fallback
if (i == 0) return "vec4(0.0, 0.0, 0.0, 1.0)";
return "out_" + std::to_string(i - 1);
};
out << " vec4 out_" << i << " = node_" << i << "(";
for (int k = 0; k < ni; ++k) {
if (k > 0) out << ", ";
out << resolve(k);
}
if (ni > 0) out << ", ";
out << "uv);\n";
}
out << " fragColor = out_" << (n - 1) << ";\n";
+414
View File
@@ -0,0 +1,414 @@
#include "gfx/dag_node_editor.h"
#include "gfx/dag_catalog.h"
#include "imgui.h"
#include "imgui_node_editor.h"
#include <algorithm>
#include <cstdio>
#include <queue>
#include <string>
#include <unordered_map>
#include <vector>
namespace ed = ax::NodeEditor;
namespace fn::gfx {
static constexpr int MAX_NODES = 16;
static ed::EditorContext* s_ctx = nullptr;
static uint32_t s_next_uid = 1;
// ── ID encoding ──────────────────────────────────────────────────────────────
// node id = editor_uid
// output pin id = (editor_uid << 8) | 0
// input pin id = (editor_uid << 8) | (slot + 1) slot 0..3
// link id = (from_node_uid << 20) | (to_node_uid << 8) | slot
static uintptr_t node_id(uint32_t uid) {
return static_cast<uintptr_t>(uid);
}
static uintptr_t output_pin_id(uint32_t uid) {
return (static_cast<uintptr_t>(uid) << 8) | 0u;
}
static uintptr_t input_pin_id(uint32_t uid, int slot) {
return (static_cast<uintptr_t>(uid) << 8) | static_cast<uintptr_t>(slot + 1);
}
static uintptr_t link_id(uint32_t from_uid, uint32_t to_uid, int slot) {
return (static_cast<uintptr_t>(from_uid) << 20)
| (static_cast<uintptr_t>(to_uid) << 8)
| static_cast<uintptr_t>(slot);
}
// Decode pin id back
static bool is_output_pin(uintptr_t id) { return (id & 0xFF) == 0; }
static uint32_t uid_from_pin(uintptr_t id) { return static_cast<uint32_t>(id >> 8); }
static int slot_from_input_pin(uintptr_t id) { return static_cast<int>(id & 0xFF) - 1; }
static int find_by_uid(const std::vector<DagStep>& p, uint32_t uid) {
for (int i = 0; i < static_cast<int>(p.size()); ++i) {
if (p[static_cast<size_t>(i)].editor_uid == uid) return i;
}
return -1;
}
static int find_by_id(const std::vector<DagStep>& p, const std::string& id) {
for (int i = 0; i < static_cast<int>(p.size()); ++i) {
if (p[static_cast<size_t>(i)].id == id) return i;
}
return -1;
}
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);
}
return ImVec4(1, 1, 1, 1);
}
// Topological sort using Kahn's algorithm.
// Returns false if a cycle is detected (should not happen — BeginCreate rejects cycles).
static bool topo_sort(std::vector<DagStep>& pipeline) {
int n = static_cast<int>(pipeline.size());
if (n == 0) return true;
// Build adjacency: edge from src -> dst if dst.source_ids[k] == src.id
std::unordered_map<std::string, int> id_to_idx;
for (int i = 0; i < n; ++i) id_to_idx[pipeline[static_cast<size_t>(i)].id] = i;
std::vector<int> in_degree(static_cast<size_t>(n), 0);
std::vector<std::vector<int>> adj(static_cast<size_t>(n));
for (int i = 0; i < n; ++i) {
const DagStep& s = pipeline[static_cast<size_t>(i)];
const DagNodeDef* def = dag_find(s.name);
int ni = def ? def->num_inputs : 0;
for (int k = 0; k < ni; ++k) {
const std::string& sid = s.source_ids[static_cast<size_t>(k)];
if (sid.empty()) continue;
auto it = id_to_idx.find(sid);
if (it == id_to_idx.end()) continue;
int src = it->second;
adj[static_cast<size_t>(src)].push_back(i);
in_degree[static_cast<size_t>(i)]++;
}
}
std::queue<int> q;
for (int i = 0; i < n; ++i) {
if (in_degree[static_cast<size_t>(i)] == 0) q.push(i);
}
std::vector<int> order;
order.reserve(static_cast<size_t>(n));
while (!q.empty()) {
int u = q.front(); q.pop();
order.push_back(u);
for (int v : adj[static_cast<size_t>(u)]) {
if (--in_degree[static_cast<size_t>(v)] == 0) q.push(v);
}
}
if (static_cast<int>(order.size()) != n) return false; // cycle
std::vector<DagStep> sorted;
sorted.reserve(static_cast<size_t>(n));
for (int idx : order) sorted.push_back(pipeline[static_cast<size_t>(idx)]);
pipeline = std::move(sorted);
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;
// Ensure UIDs are assigned (e.g. for nodes created before this editor was active)
for (auto& step : pipeline) {
if (step.editor_uid == 0) {
step.editor_uid = s_next_uid++;
}
}
// Create context on first call
if (!s_ctx) {
ed::Config cfg;
cfg.SettingsFile = nullptr; // no disk persistence
s_ctx = ed::CreateEditor(&cfg);
}
changed |= draw_add_toolbar(pipeline);
ImGui::Separator();
ed::SetCurrentEditor(s_ctx);
ed::Begin("dag_editor", ImVec2(0.0f, 0.0f));
// ── Draw nodes ───────────────────────────────────────────────────────────
for (int i = 0; i < static_cast<int>(pipeline.size()); ++i) {
DagStep& step = pipeline[static_cast<size_t>(i)];
const DagNodeDef* def = dag_find(step.name);
if (!def) continue;
ImVec4 col = kind_color(def->kind);
ed::BeginNode(ed::NodeId(node_id(step.editor_uid)));
// Header
ImGui::PushStyleColor(ImGuiCol_Text, col);
ImGui::TextUnformatted(def->label.c_str());
ImGui::PopStyleColor();
ImGui::Dummy(ImVec2(0, 2));
// Input pins (left column) + controls (middle) + output pin (right)
// We use a simple layout: input pins vertically, then controls, then output pin
int ni = def->num_inputs;
// Input pins
for (int k = 0; k < ni; ++k) {
ed::BeginPin(ed::PinId(input_pin_id(step.editor_uid, k)), ed::PinKind::Input);
char lbl[16];
std::snprintf(lbl, sizeof(lbl), "-> in%d", k);
ImGui::TextUnformatted(lbl);
ed::EndPin();
}
if (ni == 0) {
// Gen: no inputs, push a small placeholder so layout is consistent
ImGui::Dummy(ImVec2(4, 4));
}
// Controls
ImGui::PushID(static_cast<int>(step.editor_uid));
for (size_t ci = 0; ci < def->controls.size(); ++ci) {
const DagControl& ctrl = def->controls[ci];
ImGui::SetNextItemWidth(150.0f);
char uid_lbl[64];
std::snprintf(uid_lbl, sizeof(uid_lbl), "%s##%u%zu", ctrl.label.c_str(), step.editor_uid, ci);
if (ctrl.kind == DagControl::Kind::Slider) {
int pidx = ctrl.param_idx[0];
if (pidx >= 0 && pidx < 4) {
ImGui::SliderFloat(uid_lbl, &step.params[static_cast<size_t>(pidx)], ctrl.min, ctrl.max);
}
} else if (ctrl.kind == DagControl::Kind::XY) {
int px = ctrl.param_idx[0], py = ctrl.param_idx[1];
if (px >= 0 && px < 4 && py >= 0 && py < 4 && py == px + 1) {
ImGui::SliderFloat2(uid_lbl, &step.params[static_cast<size_t>(px)], ctrl.min, ctrl.max);
}
} else if (ctrl.kind == DagControl::Kind::Color) {
int pr = ctrl.param_idx[0];
if (pr >= 0 && pr + 2 < 4) {
ImGui::ColorEdit3(uid_lbl, &step.params[static_cast<size_t>(pr)]);
}
}
}
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();
ed::EndNode();
// Set initial position if not yet placed (both zero = first time)
if (step.editor_pos_x == 0.0f && step.editor_pos_y == 0.0f) {
step.editor_pos_x = 50.0f + static_cast<float>(i) * 220.0f;
step.editor_pos_y = 100.0f;
}
ed::SetNodePosition(ed::NodeId(node_id(step.editor_uid)),
ImVec2(step.editor_pos_x, step.editor_pos_y));
}
// ── Draw existing links ──────────────────────────────────────────────────
for (int i = 0; i < static_cast<int>(pipeline.size()); ++i) {
const DagStep& step = pipeline[static_cast<size_t>(i)];
const DagNodeDef* def = dag_find(step.name);
if (!def) continue;
for (int k = 0; k < def->num_inputs; ++k) {
const std::string& sid = step.source_ids[static_cast<size_t>(k)];
if (sid.empty()) continue;
int src_idx = find_by_id(pipeline, sid);
if (src_idx < 0) continue;
uint32_t src_uid = pipeline[static_cast<size_t>(src_idx)].editor_uid;
ed::Link(ed::LinkId(link_id(src_uid, step.editor_uid, k)),
ed::PinId(output_pin_id(src_uid)),
ed::PinId(input_pin_id(step.editor_uid, k)));
}
}
// ── Handle link creation ─────────────────────────────────────────────────
if (ed::BeginCreate()) {
ed::PinId start_pin, end_pin;
if (ed::QueryNewLink(&start_pin, &end_pin)) {
uintptr_t sp = start_pin.Get();
uintptr_t ep = end_pin.Get();
// Normalise: start must be output, end must be input
if (!is_output_pin(sp) && is_output_pin(ep)) {
std::swap(sp, ep);
}
bool valid = is_output_pin(sp) && !is_output_pin(ep);
if (valid) {
uint32_t from_uid = uid_from_pin(sp);
uint32_t to_uid = uid_from_pin(ep);
int slot = slot_from_input_pin(ep);
int from_idx = find_by_uid(pipeline, from_uid);
int to_idx = find_by_uid(pipeline, to_uid);
// Reject cycles: in topo-ordered list, from must come before to.
// We also reject self-loops.
if (from_uid == to_uid) {
ed::RejectNewItem(ImVec4(1, 0, 0, 1));
valid = false;
} else if (from_idx >= to_idx) {
// Would create a back-edge; reject
ed::RejectNewItem(ImVec4(1, 0.5f, 0, 1));
valid = false;
}
if (valid && from_idx >= 0 && to_idx >= 0 && slot >= 0) {
if (ed::AcceptNewItem()) {
pipeline[static_cast<size_t>(to_idx)].source_ids[static_cast<size_t>(slot)] =
pipeline[static_cast<size_t>(from_idx)].id;
changed = true;
}
}
} else {
ed::RejectNewItem(ImVec4(0.5f, 0.5f, 0.5f, 1));
}
}
}
ed::EndCreate();
// ── Handle deletion ──────────────────────────────────────────────────────
if (ed::BeginDelete()) {
ed::LinkId del_lid;
while (ed::QueryDeletedLink(&del_lid)) {
if (ed::AcceptDeletedItem()) {
uintptr_t raw = del_lid.Get();
uint32_t to_uid = static_cast<uint32_t>((raw >> 8) & 0xFFF);
int slot = static_cast<int>(raw & 0xFF);
int to_idx = find_by_uid(pipeline, to_uid);
if (to_idx >= 0 && slot < 4) {
pipeline[static_cast<size_t>(to_idx)].source_ids[static_cast<size_t>(slot)].clear();
changed = true;
}
}
}
ed::NodeId del_nid;
while (ed::QueryDeletedNode(&del_nid)) {
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();
}
}
pipeline.erase(pipeline.begin() + idx);
changed = true;
}
}
}
}
ed::EndDelete();
// ── Save node positions back to steps ────────────────────────────────────
for (auto& step : pipeline) {
auto pos = ed::GetNodePosition(ed::NodeId(node_id(step.editor_uid)));
step.editor_pos_x = pos.x;
step.editor_pos_y = pos.y;
}
ed::End();
ed::SetCurrentEditor(nullptr);
// ── Topological sort after topology change ───────────────────────────────
if (changed) {
if (!topo_sort(pipeline)) {
// Cycle detected — should not happen given BeginCreate validation
// but log a warning and leave order as-is
std::fprintf(stderr, "dag_node_editor: cycle detected, skipping topo sort\n");
}
}
return changed;
}
void dag_node_editor_destroy() {
if (s_ctx) {
ed::DestroyEditor(s_ctx);
s_ctx = nullptr;
}
}
} // namespace fn::gfx
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <vector>
#include "gfx/dag_types.h"
namespace fn::gfx {
// Renders the visual node editor (imgui-node-editor) inside the current ImGui::Begin.
// Modifies pipeline in-place: add/remove nodes, manage edges (source_ids).
// Returns true if TOPOLOGY changed (nodes or edges added/removed).
// Does not return true for position moves or slider changes.
// Call once per frame. Maintains its own editor context (static singleton).
bool dag_node_editor(std::vector<DagStep>& pipeline);
// Release editor resources. Call on shutdown.
void dag_node_editor_destroy();
} // namespace fn::gfx
+43
View File
@@ -0,0 +1,43 @@
---
name: dag_node_editor
kind: function
lang: cpp
domain: gfx
version: "1.0.0"
purity: impure
signature: "bool dag_node_editor(std::vector<DagStep>& pipeline)"
description: "Renderiza el node editor visual (imgui-node-editor) para el DAG de shaders. Modifica el pipeline in-place: añade/borra nodos, gestiona aristas (source_ids). Devuelve true si la topologia cambio."
tags: [dag, imgui, node-editor, shader, visual, pipeline, gfx]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["imgui_node_editor.h", "gfx/dag_catalog.h", "gfx/dag_types.h"]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/gfx/dag_node_editor.cpp"
params:
- name: pipeline
desc: "vector de DagStep que representa el pipeline actual; modificado in-place"
output: "true si la topologia cambio (nodos o aristas añadidos/quitados); false en caso contrario"
---
## Notas
Sustituye a `dag_panel` como UI del pipeline. Usa `ax::NodeEditor` (thedmd/imgui-node-editor v0.9.4) con contexto singleton estático.
Codificacion de IDs:
- Node id = `editor_uid` del step
- Output pin = `(uid << 8) | 0`
- Input pin = `(uid << 8) | (slot + 1)`
- Link id = `(from_uid << 20) | (to_uid << 8) | slot`
Aplica topological sort (Kahn) tras cada cambio de topologia. Rechaza ciclos en `BeginCreate`.
Multi-source: cada nodo declara `num_inputs` (0-4). Los slots de `source_ids[]` se mapean a los inputs del shader GLSL (`a`, `b`, `c`, `d`).
## Dependencia
Requiere `imgui_node_editor` static library linkeada (`cpp/vendor/imgui-node-editor`).
+4 -4
View File
@@ -76,7 +76,7 @@ bool dag_panel(std::vector<DagStep>& pipeline) {
step.params = def.param_defaults;
if (def.kind == DagKind::Blend && !pipeline.empty()) {
int src = std::max(0, static_cast<int>(pipeline.size()) - 2);
step.source_id = pipeline[static_cast<size_t>(src)].id;
step.source_ids[1] = pipeline[static_cast<size_t>(src)].id;
}
pipeline.push_back(step);
changed = true;
@@ -146,9 +146,9 @@ bool dag_panel(std::vector<DagStep>& pipeline) {
}
int current_src = std::max(0, i - 2);
if (!step.source_id.empty()) {
if (!step.source_ids[1].empty()) {
for (int j = 0; j < i; ++j) {
if (pipeline[static_cast<size_t>(j)].id == step.source_id) {
if (pipeline[static_cast<size_t>(j)].id == step.source_ids[1]) {
current_src = j;
break;
}
@@ -161,7 +161,7 @@ bool dag_panel(std::vector<DagStep>& pipeline) {
int sel = current_src;
if (ImGui::Combo("Source", &sel, items.data(), static_cast<int>(items.size()))) {
if (sel >= 0 && sel < i) {
step.source_id = pipeline[static_cast<size_t>(sel)].id;
step.source_ids[1] = pipeline[static_cast<size_t>(sel)].id;
changed = true;
}
}
+5 -1
View File
@@ -23,6 +23,7 @@ struct DagNodeDef {
std::string label;
std::string desc;
DagKind kind = DagKind::Gen;
int num_inputs = 0; // 0=Gen, 1=Op, 2=Blend, up to 4
std::array<std::string, 4> param_names{"", "", "", ""};
std::array<float, 4> param_defaults{0, 0, 0, 0};
std::vector<DagControl> controls;
@@ -33,7 +34,10 @@ struct DagStep {
std::string id;
std::string name;
std::array<float, 4> params{0, 0, 0, 0};
std::string source_id;
std::array<std::string, 4> source_ids{"", "", "", ""}; // up to 4 inputs; "" = no connection
float editor_pos_x = 0.0f;
float editor_pos_y = 0.0f;
uint32_t editor_uid = 0; // monotonic counter, used as node editor ID
};
} // namespace fn::gfx