#include "gfx/dag_node_editor.h" #include "gfx/dag_catalog.h" #include "imgui.h" #include "imgui_node_editor.h" #include #include #include #include #include #include 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(uid); } static uintptr_t output_pin_id(uint32_t uid) { return (static_cast(uid) << 8) | 0u; } static uintptr_t input_pin_id(uint32_t uid, int slot) { return (static_cast(uid) << 8) | static_cast(slot + 1); } static uintptr_t link_id(uint32_t from_uid, uint32_t to_uid, int slot) { return (static_cast(from_uid) << 20) | (static_cast(to_uid) << 8) | static_cast(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(id >> 8); } static int slot_from_input_pin(uintptr_t id) { return static_cast(id & 0xFF) - 1; } static int find_by_uid(const std::vector& p, uint32_t uid) { for (int i = 0; i < static_cast(p.size()); ++i) { if (p[static_cast(i)].editor_uid == uid) return i; } return -1; } static int find_by_id(const std::vector& p, const std::string& id) { for (int i = 0; i < static_cast(p.size()); ++i) { if (p[static_cast(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& pipeline) { int n = static_cast(pipeline.size()); if (n == 0) return true; // Build adjacency: edge from src -> dst if dst.source_ids[k] == src.id std::unordered_map id_to_idx; for (int i = 0; i < n; ++i) id_to_idx[pipeline[static_cast(i)].id] = i; std::vector in_degree(static_cast(n), 0); std::vector> adj(static_cast(n)); for (int i = 0; i < n; ++i) { const DagStep& s = pipeline[static_cast(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(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(src)].push_back(i); in_degree[static_cast(i)]++; } } std::queue q; for (int i = 0; i < n; ++i) { if (in_degree[static_cast(i)] == 0) q.push(i); } std::vector order; order.reserve(static_cast(n)); while (!q.empty()) { int u = q.front(); q.pop(); order.push_back(u); for (int v : adj[static_cast(u)]) { if (--in_degree[static_cast(v)] == 0) q.push(v); } } if (static_cast(order.size()) != n) return false; // cycle std::vector sorted; sorted.reserve(static_cast(n)); for (int idx : order) sorted.push_back(pipeline[static_cast(idx)]); pipeline = std::move(sorted); 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; // 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(pipeline.size()); ++i) { DagStep& step = pipeline[static_cast(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(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(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(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(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(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(pipeline.size()); ++i) { const DagStep& step = pipeline[static_cast(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(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(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(to_idx)].source_ids[static_cast(slot)] = pipeline[static_cast(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((raw >> 8) & 0xFFF); int slot = static_cast(raw & 0xFF); int to_idx = find_by_uid(pipeline, to_uid); if (to_idx >= 0 && slot < 4) { pipeline[static_cast(to_idx)].source_ids[static_cast(slot)].clear(); changed = true; } } } ed::NodeId del_nid; while (ed::QueryDeletedNode(&del_nid)) { 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(); } } 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