Files
fn_registry/cpp/functions/gfx/dag_node_editor.cpp
T
egutierrez e91e80bfcf fix(shaders_lab): node drag was overwritten by SetNodePosition each frame
Previously SetNodePosition was called after EndNode every frame, which
reset any drag the user had done. Now the initial position is set once
per editor_uid (tracked in a static unordered_set), and the editor owns
the position afterwards. GetNodePosition at end-of-frame keeps step
state in sync for future persistence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:04:07 +02:00

421 lines
16 KiB
C++

#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 <unordered_set>
#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;
static std::unordered_set<uint32_t> s_positioned;
// ── 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);
// Initial position only — after that, the editor owns the node's position
// and user drags must not be overwritten.
if (s_positioned.find(step.editor_uid) == s_positioned.end()) {
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));
s_positioned.insert(step.editor_uid);
}
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();
}
// ── 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