diff --git a/cpp/functions/gfx/dag_node_editor.cpp b/cpp/functions/gfx/dag_node_editor.cpp index fde8245d..61c58586 100644 --- a/cpp/functions/gfx/dag_node_editor.cpp +++ b/cpp/functions/gfx/dag_node_editor.cpp @@ -70,6 +70,22 @@ static ImVec4 kind_color(DagKind kind) { return ImVec4(1, 1, 1, 1); } +static constexpr float PIN_RADIUS = 7.0f; +static constexpr float PIN_DIAMETER = PIN_RADIUS * 2.0f; + +// Draws a filled circle of `color` and reserves a 14x14 dummy item so the +// node editor can compute the pin's hit rect from the surrounding BeginPin. +// Adds an outline in a slightly darker tone for visibility. +static void draw_pin_circle(ImVec4 color) { + ImVec2 cursor = ImGui::GetCursorScreenPos(); + ImVec2 center = ImVec2(cursor.x + PIN_RADIUS, cursor.y + PIN_RADIUS); + ImDrawList* dl = ImGui::GetWindowDrawList(); + dl->AddCircleFilled(center, PIN_RADIUS, ImGui::ColorConvertFloat4ToU32(color)); + ImVec4 border(color.x * 0.4f, color.y * 0.4f, color.z * 0.4f, 1.0f); + dl->AddCircle(center, PIN_RADIUS, ImGui::ColorConvertFloat4ToU32(border), 0, 1.5f); + ImGui::Dummy(ImVec2(PIN_DIAMETER, PIN_DIAMETER)); +} + // 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) { @@ -225,26 +241,30 @@ bool dag_node_editor(std::vector& pipeline) { 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; + bool has_output_pin = (def->kind != DagKind::Output); - // Input pins + // ── Three-column layout: inputs · controls · output ────────── + ImGui::BeginGroup(); // inputs column + if (ni == 0) { + ImGui::Dummy(ImVec2(PIN_DIAMETER, PIN_DIAMETER)); + } 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::PinPivotAlignment(ImVec2(0.0f, 0.5f)); + ed::PinPivotSize(ImVec2(0, 0)); + draw_pin_circle(col); ed::EndPin(); } + ImGui::EndGroup(); - if (ni == 0) { - // Gen: no inputs, push a small placeholder so layout is consistent - ImGui::Dummy(ImVec2(4, 4)); - } + ImGui::SameLine(); - // Controls + ImGui::BeginGroup(); // controls column ImGui::PushID(static_cast(step.editor_uid)); + if (def->controls.empty() && def->kind != DagKind::Output) { + ImGui::Dummy(ImVec2(60, PIN_DIAMETER)); + } for (size_t ci = 0; ci < def->controls.size(); ++ci) { const DagControl& ctrl = def->controls[ci]; ImGui::SetNextItemWidth(150.0f); @@ -263,9 +283,6 @@ bool dag_node_editor(std::vector& pipeline) { } else if (ctrl.kind == DagControl::Kind::Color) { int pr = ctrl.param_idx[0]; if (pr >= 0 && pr + 2 < 4) { - // Suspend the node editor while the color picker popup is - // open so its clicks don't reach the canvas (and so the - // popup isn't clipped to the node bounds). ed::Suspend(); ImGui::ColorEdit3(uid_lbl, &step.params[static_cast(pr)], ImGuiColorEditFlags_NoInputs | @@ -276,14 +293,21 @@ bool dag_node_editor(std::vector& pipeline) { } } ImGui::PopID(); + ImGui::EndGroup(); - // Output pin (skip for the terminal Output node — it has no output) - if (def->kind != DagKind::Output) { - ImGui::Dummy(ImVec2(0, 2)); + ImGui::SameLine(); + + ImGui::BeginGroup(); // output column + if (has_output_pin) { ed::BeginPin(ed::PinId(output_pin_id(step.editor_uid)), ed::PinKind::Output); - ImGui::Text("out ->"); + ed::PinPivotAlignment(ImVec2(1.0f, 0.5f)); + ed::PinPivotSize(ImVec2(0, 0)); + draw_pin_circle(col); ed::EndPin(); + } else { + ImGui::Dummy(ImVec2(PIN_DIAMETER, PIN_DIAMETER)); } + ImGui::EndGroup(); ed::EndNode(); } @@ -298,13 +322,33 @@ bool dag_node_editor(std::vector& pipeline) { 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))); + const DagStep& src_step = pipeline[static_cast(src_idx)]; + const DagNodeDef* src_def = dag_find(src_step.name); + ImVec4 link_col = src_def ? kind_color(src_def->kind) : ImVec4(0.7f, 0.7f, 0.7f, 1.0f); + ed::Link(ed::LinkId(link_id(src_step.editor_uid, step.editor_uid, k)), + ed::PinId(output_pin_id(src_step.editor_uid)), + ed::PinId(input_pin_id(step.editor_uid, k)), + link_col, 2.5f); } } + // ── Right-click on a link deletes it directly ────────────────────────── + { + ed::Suspend(); + ed::LinkId ctx_lid; + if (ed::ShowLinkContextMenu(&ctx_lid)) { + uintptr_t raw = ctx_lid.Get(); + uint32_t to_uid = static_cast((raw >> 8) & 0xFFFu); + int slot = static_cast(raw & 0xFFu); + int to_idx = find_by_uid(pipeline, to_uid); + if (to_idx >= 0 && slot >= 0 && slot < 4) { + pipeline[static_cast(to_idx)].source_ids[static_cast(slot)].clear(); + changed = true; + } + } + ed::Resume(); + } + // ── Handle link creation ───────────────────────────────────────────────── if (ed::BeginCreate()) { ed::PinId start_pin, end_pin;