From 7ae0a554dc61eefabb98ac433274a8eaeb5d64cd Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Wed, 25 Mar 2026 22:06:43 +0200 Subject: [PATCH 01/86] wire management: add link name editing and auto-wire functionality --- scenes/klavier/main.atto | 13 +-- src/atto/model.h | 1 + src/atto/serial.cpp | 3 + src/atto/shadow.cpp | 50 +++++++++ src/attoflow/editor.cpp | 215 +++++++++++++++++++++++++++++++++++++-- src/attoflow/editor.h | 5 + src/attoflow/main.cpp | 2 +- 7 files changed, 271 insertions(+), 18 deletions(-) diff --git a/scenes/klavier/main.atto b/scenes/klavier/main.atto index fadeecc..624fff0 100644 --- a/scenes/klavier/main.atto +++ b/scenes/klavier/main.atto @@ -293,7 +293,7 @@ id = "$auto-45df349a6ae05f56" type = "store!" args = ["$delay_line_size"] inputs = ["$auto-ddaf4497489a54c2-bang0", "$auto-45df349a6ae05f56_s1-out0"] -outputs = ["$auto-45df349a6ae05f56-bang0"] +outputs = ["$step2"] position = [785.327, 1265.73] [[node]] @@ -411,9 +411,9 @@ position = [1284.42, 1561.27] id = "$auto-f79f6fb421f321f0" type = "store!" args = ["$klavie_down"] -inputs = ["$auto-45df349a6ae05f56-bang0", "$auto-daa77173e91ec011-as_lambda"] +inputs = ["$step2", "$auto-daa77173e91ec011-as_lambda"] outputs = ["$auto-f79f6fb421f321f0-bang0"] -position = [1224.44, 1270.42] +position = [1186.87, 1217.83] [[node]] id = "$auto-487ba455bf42a84c" @@ -1183,7 +1183,7 @@ position = [1749.22, 1038.66] id = "$auto-7837ca36997a9a3d" type = "decl_var" inputs = ["", "$auto-7837ca36997a9a3d_s0-out0", "$auto-7837ca36997a9a3d_s1-out0"] -position = [1419.58, 1130.03] +position = [1412.31, 1178.21] [[node]] id = "$auto-7837ca36997a9a3d_s0" @@ -1249,11 +1249,6 @@ args = ["array"] outputs = ["$auto-7837ca36997a9a3d_s1-out0"] position = [1060.74, 944.346] -[[node]] -id = "$auto-206413c1566d4d28" -type = "decl_var" -position = [1385.62, 980.6] - [[node]] id = "$auto-960e1ea09eeca09e" type = "expr" diff --git a/src/atto/model.h b/src/atto/model.h index b20513a..60725ef 100644 --- a/src/atto/model.h +++ b/src/atto/model.h @@ -126,6 +126,7 @@ struct FlowLink { std::string from_pin; // output pin id string (for serialization) std::string to_pin; // input pin id string (for serialization) std::string net_name; // named net this link belongs to (v2 format, e.g. "$my-signal") + bool auto_wire = false; // true for auto-generated nets (not shown in display) std::string error; // non-empty if this link has a type error (set during inference) // Resolved pointers — populated by GraphIndex::rebuild(), not serialized FlowPin* from = nullptr; diff --git a/src/atto/serial.cpp b/src/atto/serial.cpp index d1fa02f..0d7c126 100644 --- a/src/atto/serial.cpp +++ b/src/atto/serial.cpp @@ -261,8 +261,10 @@ static void migrate_v1_to_v2(FlowGraph& graph) { } if (!existing_net.empty()) { link.net_name = existing_net; + link.auto_wire = true; } else { link.net_name = source_node_id + "-" + pin_name; + link.auto_wire = true; } } } @@ -613,6 +615,7 @@ static bool load_v2_stream(std::istream& f, FlowGraph& graph, const std::string& for (auto& link : graph.links) { if (link.id == link_id) { link.net_name = net_name; + link.auto_wire = (net_name.substr(0, 6) == "$auto-"); break; } } diff --git a/src/atto/shadow.cpp b/src/atto/shadow.cpp index bff11db..1aa9f9c 100644 --- a/src/atto/shadow.cpp +++ b/src/atto/shadow.cpp @@ -349,6 +349,32 @@ void rebuild_all_inline_display(FlowGraph& graph) { std::sort(infos.begin(), infos.end(), [](auto& a, auto& b) { return a.arg_index < b.arg_index; }); } + // Helper: find the user-named net connected to a node's descriptor input pin + auto find_connected_net = [&](const FlowNode& node, const std::string& pin_name) -> std::string { + std::string pin_id = node.guid + "." + pin_name; + for (auto& link : graph.links) { + if (link.to_pin == pin_id && !link.net_name.empty() && !link.auto_wire) { + return link.net_name; + } + } + return ""; + }; + + // Helper: find the node_id of a node connected via as_lambda to a given pin + auto find_connected_lambda_id = [&](const FlowNode& node, const std::string& pin_name) -> std::string { + std::string pin_id = node.guid + "." + pin_name; + for (auto& link : graph.links) { + if (link.to_pin != pin_id) continue; + // Find source node — the from_pin should be "guid.as_lambda" + for (auto& src : graph.nodes) { + if (src.lambda_grab.id == link.from_pin && !src.node_id.empty()) { + return src.node_id; + } + } + } + return ""; + }; + // Rebuild inline_display for all non-shadow nodes for (auto& node : graph.nodes) { if (node.shadow) continue; @@ -374,9 +400,33 @@ void rebuild_all_inline_display(FlowGraph& graph) { for (auto& si : it->second) tokens[si.arg_index] = si.expr; + // Substitute net names for shadow tokens connected via nets + auto* nt = find_node_type(node.type_id); + for (auto& si : it->second) { + if (si.arg_index < (int)tokens.size() && nt && nt->input_ports && si.arg_index < nt->inputs) { + std::string pin_name = nt->input_ports[si.arg_index].name; + std::string net = find_connected_net(node, pin_name); + if (!net.empty()) { + tokens[si.arg_index] = net; + } + } + } + for (auto& t : tokens) { s += " " + t; } + + // Append lambda pin references (show $node-id for connected lambdas) + if (nt && nt->input_ports) { + for (int i = 0; i < nt->inputs; i++) { + if (nt->input_ports[i].kind == PortKind::Lambda) { + std::string lambda_id = find_connected_lambda_id(node, nt->input_ports[i].name); + if (!lambda_id.empty()) { + s += " " + lambda_id; + } + } + } + } } else if (!node.args.empty()) { s += " " + node.args; } diff --git a/src/attoflow/editor.cpp b/src/attoflow/editor.cpp index b8599b5..1c22730 100644 --- a/src/attoflow/editor.cpp +++ b/src/attoflow/editor.cpp @@ -208,6 +208,65 @@ static void draw_vbezier(ImDrawList* dl, ImVec2 from, ImVec2 to, ImU32 col, floa dl->AddBezierCubic(from, {from.x, from.y + dy}, {to.x, to.y - dy}, to, col, thickness * zoom); } +// Sample a cubic bezier at parameter t +static ImVec2 bezier_sample(ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3, float t) { + float u = 1.0f - t; + float uu = u * u, uuu = uu * u; + float tt = t * t, ttt = tt * t; + return {uuu*p0.x + 3*uu*t*p1.x + 3*u*tt*p2.x + ttt*p3.x, + uuu*p0.y + 3*uu*t*p1.y + 3*u*tt*p2.y + ttt*p3.y}; +} + +static void draw_dashed_bezier(ImDrawList* dl, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3, + ImU32 col, float thickness, float dash_len, float gap_len) { + const int samples = 64; + // Build polyline with arc-length parameterization + ImVec2 pts[samples + 1]; + float lengths[samples + 1]; + pts[0] = p0; + lengths[0] = 0; + for (int i = 1; i <= samples; i++) { + pts[i] = bezier_sample(p0, p1, p2, p3, (float)i / samples); + float dx = pts[i].x - pts[i-1].x, dy = pts[i].y - pts[i-1].y; + lengths[i] = lengths[i-1] + sqrtf(dx*dx + dy*dy); + } + float total = lengths[samples]; + float cycle = dash_len + gap_len; + + // Walk along the curve drawing dash segments + int seg = 0; + float dist = 0; + while (dist < total) { + float dash_start = dist; + float dash_end = std::min(dist + dash_len, total); + // Find segment indices for start and end + // Draw line segments between sampled points in the dash range + bool in_dash = false; + ImVec2 prev = {}; + for (int i = 0; i <= samples; i++) { + if (lengths[i] >= dash_start && lengths[i] <= dash_end) { + if (!in_dash) { + // Interpolate exact start point + prev = pts[i]; + in_dash = true; + } else { + dl->AddLine(prev, pts[i], col, thickness); + prev = pts[i]; + } + } else if (in_dash) { + break; + } + } + dist += cycle; + } +} + +static void draw_dashed_vbezier(ImDrawList* dl, ImVec2 from, ImVec2 to, ImU32 col, float thickness, float zoom) { + float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * zoom); + draw_dashed_bezier(dl, from, {from.x, from.y + dy}, {to.x, to.y - dy}, to, + col, thickness * zoom, 8.0f * zoom, 4.0f * zoom); +} + bool FlowEditorWindow::init(const std::string& project_dir) { if (!win_.init("Flow Editor", 900, 600)) return false; project_dir_ = project_dir; @@ -661,6 +720,24 @@ void FlowEditorWindow::draw_link(ImDrawList* dl, const FlowLink& link, ImVec2 or } else { draw_vbezier(dl, fp, tp, type_error ? col_error : COL_LINK, 2.5f, active().canvas_zoom); } + + // Draw net name label at midpoint if the wire has a user-assigned name + if (!link.net_name.empty() && !link.auto_wire) { + float font_size = ImGui::GetFontSize() * active().canvas_zoom * 0.8f; + if (font_size > 5.0f) { + // Compute midpoint of the bezier (approximate with lerp) + ImVec2 mid = {(fp.x + tp.x) * 0.5f, (fp.y + tp.y) * 0.5f}; + ImVec2 text_sz = ImGui::CalcTextSize(link.net_name.c_str()); + float tw = text_sz.x * (font_size / ImGui::GetFontSize()); + float th = text_sz.y * (font_size / ImGui::GetFontSize()); + float cx = mid.x - tw * 0.5f; + float cy = mid.y - th * 0.5f; + // Background pill + dl->AddRectFilled({cx - 3, cy - 1}, {cx + tw + 3, cy + th + 1}, + IM_COL32(30, 30, 40, 200), 3.0f); + dl->AddText(nullptr, font_size, {cx, cy}, IM_COL32(180, 220, 255, 255), link.net_name.c_str()); + } + } } void FlowEditorWindow::draw() { @@ -875,7 +952,7 @@ void FlowEditorWindow::draw() { } } // --- Single click --- - else if (canvas_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + else if (canvas_hovered && editing_link_ < 0 && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { if (editing_node_ >= 0) { if (creating_new_node_ && editing_node_ > 0) active().graph.remove_node(editing_node_); editing_node_ = -1; @@ -908,12 +985,20 @@ void FlowEditorWindow::draw() { dragging_node_ = -1; } } else { - // Click empty space: start potential box select - // If released without dragging: deselect (if selected) or create node - box_selecting_ = true; - box_select_start_ = mouse_pos; - dragging_node_ = -1; - dragging_selection_ = false; + // Check if clicking a wire — if so, don't start box select + int wire_hit = hit_test_link(mouse_pos, canvas_origin); + if (wire_hit >= 0) { + // Wire clicked — will be handled by link rename on mouse up + dragging_node_ = -1; + dragging_selection_ = false; + } else { + // Click empty space: start potential box select + // If released without dragging: deselect (if selected) or create node + box_selecting_ = true; + box_select_start_ = mouse_pos; + dragging_node_ = -1; + dragging_selection_ = false; + } } } } @@ -1343,7 +1428,7 @@ void FlowEditorWindow::draw() { } // --- Tooltips --- - if (canvas_hovered && editing_node_ < 0) { + if (canvas_hovered && editing_node_ < 0 && editing_link_ < 0) { if (!hovered_pin.pin_id.empty()) { // Pin tooltip for (auto& node : active().graph.nodes) { @@ -1407,13 +1492,25 @@ void FlowEditorWindow::draw() { ImGui::BeginTooltip(); ImGui::SetWindowFontScale(active().canvas_zoom); + // Show net name prominently if it has one + if (!link.net_name.empty()) { + ImGui::TextColored({0.7f, 0.9f, 1.0f, 1.0f}, "%s", link.net_name.c_str()); + } ImGui::TextUnformatted((from_label + " -> " + to_label).c_str()); ImGui::TextDisabled("%s -> %s", from_type_str.c_str(), to_type_str.c_str()); if (!link.error.empty()) ImGui::TextColored({1.0f, 0.2f, 0.2f, 1.0f}, "%s", link.error.c_str()); else if (type_err) ImGui::TextColored({1.0f, 0.2f, 0.2f, 1.0f}, "Type mismatch!"); + ImGui::TextDisabled("Click to rename wire"); ImGui::EndTooltip(); + + // Left-click on wire opens rename editor (on release) + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + editing_link_ = link.id; + link_edit_buf_ = link.net_name.empty() ? "$" : link.net_name; + link_edit_just_opened_ = true; + } } break; } @@ -1778,6 +1875,108 @@ void FlowEditorWindow::draw() { } // end of edit window block } + // --- Wire name editing popup --- + if (editing_link_ >= 0) { + FlowLink* edit_link = nullptr; + for (auto& link : active().graph.links) { + if (link.id == editing_link_) { edit_link = &link; break; } + } + if (!edit_link) { + editing_link_ = -1; + } else { + // Position popup near the wire midpoint + ImVec2 fp = {}, tp = {}; + for (auto& n : active().graph.nodes) { + for (auto& p : n.outputs) if (p->id == edit_link->from_pin) fp = get_pin_pos(n, *p, canvas_origin); + for (auto& p : n.nexts) if (p->id == edit_link->from_pin) fp = get_pin_pos(n, *p, canvas_origin); + if (n.lambda_grab.id == edit_link->from_pin) fp = get_pin_pos(n, n.lambda_grab, canvas_origin); + if (n.bang_pin.id == edit_link->from_pin) fp = get_pin_pos(n, n.bang_pin, canvas_origin); + for (auto& p : n.inputs) if (p->id == edit_link->to_pin) tp = get_pin_pos(n, *p, canvas_origin); + for (auto& p : n.triggers) if (p->id == edit_link->to_pin) tp = get_pin_pos(n, *p, canvas_origin); + } + ImVec2 mid = {(fp.x + tp.x) * 0.5f, (fp.y + tp.y) * 0.5f}; + + float text_w = ImGui::CalcTextSize(link_edit_buf_.c_str()).x * active().canvas_zoom + 40.0f * active().canvas_zoom; + float popup_w = std::max(200.0f * active().canvas_zoom, text_w); + ImGui::SetNextWindowPos({mid.x - popup_w * 0.5f, mid.y - 15.0f * active().canvas_zoom}); + ImGui::SetNextWindowSize({popup_w, 0}); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {4 * active().canvas_zoom, 4 * active().canvas_zoom}); + ImGui::Begin("##wire_rename", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar); + ImGui::SetWindowFontScale(active().canvas_zoom); + + bool was_just_opened = link_edit_just_opened_; + if (link_edit_just_opened_) { + ImGui::SetKeyboardFocusHere(); + link_edit_just_opened_ = false; + } + + char buf[128]; + strncpy(buf, link_edit_buf_.c_str(), sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + + bool committed = ImGui::InputText("##wire_name", buf, sizeof(buf), + ImGuiInputTextFlags_EnterReturnsTrue); + link_edit_buf_ = buf; + + // Validate: must start with $, must be unique among net names + bool valid = true; + std::string error_msg; + std::string new_name = link_edit_buf_; + if (new_name.empty() || new_name[0] != '$') { + valid = false; + error_msg = "Must start with $"; + } else if (new_name.size() < 2) { + valid = false; + error_msg = "Name too short"; + } else { + // Check uniqueness: no other link with a different source pin should have this net name + for (auto& other : active().graph.links) { + if (other.id == edit_link->id) continue; + if (other.net_name == new_name && other.from_pin != edit_link->from_pin) { + valid = false; + error_msg = "Name already in use"; + break; + } + } + } + + if (!valid && !error_msg.empty()) { + ImGui::TextColored({1.0f, 0.3f, 0.3f, 1.0f}, "%s", error_msg.c_str()); + } + + if (committed && valid) { + // Update net name on this link AND all links from the same source pin + std::string old_from = edit_link->from_pin; + for (auto& link : active().graph.links) { + if (link.from_pin == old_from) { + link.net_name = new_name; + link.auto_wire = false; + } + } + editing_link_ = -1; + rebuild_all_inline_display(active().graph); + mark_dirty(); + } + + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { + editing_link_ = -1; + } + + // Dismiss if clicked outside the rename window (skip first frame) + if (!was_just_opened && + !ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && + ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + editing_link_ = -1; + } + + ImGui::End(); + ImGui::PopStyleVar(1); + } + } + ImGui::EndChild(); // flow_canvas // --- Horizontal splitter (between canvas and bottom panel) --- diff --git a/src/attoflow/editor.h b/src/attoflow/editor.h index ea59949..1e1440b 100644 --- a/src/attoflow/editor.h +++ b/src/attoflow/editor.h @@ -130,6 +130,11 @@ class FlowEditorWindow { bool creating_new_node_ = false; ImVec2 new_node_pos_; + // Link/wire name editing + int editing_link_ = -1; + std::string link_edit_buf_; + bool link_edit_just_opened_ = false; + // Shadow pin filtering (rebuilt each frame before drawing) std::set shadow_connected_pins_; // pin IDs connected from shadow nodes diff --git a/src/attoflow/main.cpp b/src/attoflow/main.cpp index 01a40c3..5e44c50 100644 --- a/src/attoflow/main.cpp +++ b/src/attoflow/main.cpp @@ -43,7 +43,7 @@ int main(int argc, char* argv[]) { } } - editor.shutdown(); + editor.shutdown(); SDL_Quit(); return 0; } From 6a2d5ab27a3584f2a18b5ef632599f4b5b00bf84 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Wed, 25 Mar 2026 22:10:39 +0200 Subject: [PATCH 02/86] named style --- scenes/klavier/main.atto | 2 +- src/attoflow/editor.cpp | 122 ++++++++++++++++++++++++--------------- 2 files changed, 77 insertions(+), 47 deletions(-) diff --git a/scenes/klavier/main.atto b/scenes/klavier/main.atto index 624fff0..bd8eb4f 100644 --- a/scenes/klavier/main.atto +++ b/scenes/klavier/main.atto @@ -413,7 +413,7 @@ type = "store!" args = ["$klavie_down"] inputs = ["$step2", "$auto-daa77173e91ec011-as_lambda"] outputs = ["$auto-f79f6fb421f321f0-bang0"] -position = [1186.87, 1217.83] +position = [1184.17, 1184.67] [[node]] id = "$auto-487ba455bf42a84c" diff --git a/src/attoflow/editor.cpp b/src/attoflow/editor.cpp index 1c22730..6ec968b 100644 --- a/src/attoflow/editor.cpp +++ b/src/attoflow/editor.cpp @@ -219,45 +219,49 @@ static ImVec2 bezier_sample(ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3, float t) static void draw_dashed_bezier(ImDrawList* dl, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3, ImU32 col, float thickness, float dash_len, float gap_len) { - const int samples = 64; - // Build polyline with arc-length parameterization - ImVec2 pts[samples + 1]; - float lengths[samples + 1]; - pts[0] = p0; - lengths[0] = 0; - for (int i = 1; i <= samples; i++) { - pts[i] = bezier_sample(p0, p1, p2, p3, (float)i / samples); + const int N = 128; + // Pre-sample curve points and cumulative arc lengths + ImVec2 pts[N + 1]; + float arc[N + 1]; + pts[0] = p0; arc[0] = 0; + for (int i = 1; i <= N; i++) { + pts[i] = bezier_sample(p0, p1, p2, p3, (float)i / N); float dx = pts[i].x - pts[i-1].x, dy = pts[i].y - pts[i-1].y; - lengths[i] = lengths[i-1] + sqrtf(dx*dx + dy*dy); + arc[i] = arc[i-1] + sqrtf(dx*dx + dy*dy); } - float total = lengths[samples]; + float total = arc[N]; + if (total < 1.0f) return; float cycle = dash_len + gap_len; - // Walk along the curve drawing dash segments - int seg = 0; - float dist = 0; - while (dist < total) { - float dash_start = dist; - float dash_end = std::min(dist + dash_len, total); - // Find segment indices for start and end - // Draw line segments between sampled points in the dash range - bool in_dash = false; - ImVec2 prev = {}; - for (int i = 0; i <= samples; i++) { - if (lengths[i] >= dash_start && lengths[i] <= dash_end) { - if (!in_dash) { - // Interpolate exact start point - prev = pts[i]; - in_dash = true; - } else { - dl->AddLine(prev, pts[i], col, thickness); - prev = pts[i]; - } - } else if (in_dash) { - break; - } - } - dist += cycle; + // Interpolate a point at a given arc distance + auto lerp_at = [&](float d) -> ImVec2 { + if (d <= 0) return pts[0]; + if (d >= total) return pts[N]; + // Binary search for segment + int lo = 0, hi = N; + while (lo < hi - 1) { int mid = (lo+hi)/2; if (arc[mid] < d) lo = mid; else hi = mid; } + float seg_len = arc[hi] - arc[lo]; + float t = (seg_len > 0) ? (d - arc[lo]) / seg_len : 0; + return {pts[lo].x + t * (pts[hi].x - pts[lo].x), + pts[lo].y + t * (pts[hi].y - pts[lo].y)}; + }; + + // Draw dashes + float d = 0; + while (d < total) { + float d_end = std::min(d + dash_len, total); + // Draw the dash as a series of short line segments + ImVec2 prev = lerp_at(d); + float step = 3.0f; // pixels per sub-segment + for (float dd = d + step; dd <= d_end; dd += step) { + ImVec2 cur = lerp_at(dd); + dl->AddLine(prev, cur, col, thickness); + prev = cur; + } + // Final segment to exact end + ImVec2 end = lerp_at(d_end); + dl->AddLine(prev, end, col, thickness); + d += cycle; } } @@ -697,28 +701,54 @@ void FlowEditorWindow::draw_link(ImDrawList* dl, const FlowLink& link, ImVec2 or bool from_trigger = from_pin_ptr && from_pin_ptr->direction == FlowPin::BangTrigger; ImU32 col_error = IM_COL32(255, 60, 60, 220); + bool named = !link.net_name.empty() && !link.auto_wire; + + // Dim named wires so the label stands out more + auto dim = [](ImU32 c) -> ImU32 { + return (c & 0x00FFFFFF) | (((c >> 24) * 100 / 255) << 24); + }; + if (named) col_error = dim(col_error); + + auto wire_col = [&](ImU32 c) { return named ? dim(c) : c; }; if (from_trigger) { - // Trigger-as-source: exits upward from top, curves to target float dy = std::max(std::abs(tp.y - fp.y) * 0.5f, 40.0f * active().canvas_zoom); - float dx = std::max(std::abs(tp.x - fp.x) * 0.3f, 20.0f * active().canvas_zoom); - dl->AddBezierCubic(fp, {fp.x, fp.y - dy}, {tp.x, tp.y - dy}, tp, - type_error ? col_error : IM_COL32(255, 200, 80, 200), 2.5f * active().canvas_zoom); + ImU32 col = type_error ? col_error : wire_col(IM_COL32(255, 200, 80, 200)); + float th = 2.5f * active().canvas_zoom; + if (named) + draw_dashed_bezier(dl, fp, {fp.x, fp.y - dy}, {tp.x, tp.y - dy}, tp, col, th, 8.0f * active().canvas_zoom, 4.0f * active().canvas_zoom); + else + dl->AddBezierCubic(fp, {fp.x, fp.y - dy}, {tp.x, tp.y - dy}, tp, col, th); } else if (from_grab) { float dx = std::max(std::abs(tp.x - fp.x) * 0.5f, 30.0f * active().canvas_zoom); float dy = std::max(std::abs(tp.y - fp.y) * 0.5f, 30.0f * active().canvas_zoom); - dl->AddBezierCubic(fp, {fp.x - dx, fp.y}, {tp.x, tp.y - dy}, tp, - type_error ? col_error : IM_COL32(180, 130, 255, 200), 2.5f * active().canvas_zoom); + ImU32 col = type_error ? col_error : wire_col(IM_COL32(180, 130, 255, 200)); + float th = 2.5f * active().canvas_zoom; + if (named) + draw_dashed_bezier(dl, fp, {fp.x - dx, fp.y}, {tp.x, tp.y - dy}, tp, col, th, 8.0f * active().canvas_zoom, 4.0f * active().canvas_zoom); + else + dl->AddBezierCubic(fp, {fp.x - dx, fp.y}, {tp.x, tp.y - dy}, tp, col, th); } else if (from_bang_pin) { float dx = std::max(std::abs(tp.x - fp.x) * 0.5f, 30.0f * active().canvas_zoom); float dy = std::max(std::abs(tp.y - fp.y) * 0.5f, 30.0f * active().canvas_zoom); - // Side bang exits horizontally right, enters bang input vertically from above - dl->AddBezierCubic(fp, {fp.x + dx, fp.y}, {tp.x, tp.y - dy}, tp, - type_error ? col_error : IM_COL32(255, 200, 80, 200), 2.5f * active().canvas_zoom); + ImU32 col = type_error ? col_error : wire_col(IM_COL32(255, 200, 80, 200)); + float th = 2.5f * active().canvas_zoom; + if (named) + draw_dashed_bezier(dl, fp, {fp.x + dx, fp.y}, {tp.x, tp.y - dy}, tp, col, th, 8.0f * active().canvas_zoom, 4.0f * active().canvas_zoom); + else + dl->AddBezierCubic(fp, {fp.x + dx, fp.y}, {tp.x, tp.y - dy}, tp, col, th); } else if (to_lambda) { - draw_vbezier(dl, fp, tp, type_error ? col_error : IM_COL32(180, 130, 255, 200), 2.5f, active().canvas_zoom); + ImU32 col = type_error ? col_error : wire_col(IM_COL32(180, 130, 255, 200)); + if (named) + draw_dashed_vbezier(dl, fp, tp, col, 2.5f, active().canvas_zoom); + else + draw_vbezier(dl, fp, tp, col, 2.5f, active().canvas_zoom); } else { - draw_vbezier(dl, fp, tp, type_error ? col_error : COL_LINK, 2.5f, active().canvas_zoom); + ImU32 col = type_error ? col_error : wire_col(COL_LINK); + if (named) + draw_dashed_vbezier(dl, fp, tp, col, 2.5f, active().canvas_zoom); + else + draw_vbezier(dl, fp, tp, col, 2.5f, active().canvas_zoom); } // Draw net name label at midpoint if the wire has a user-assigned name From 76852d2207dc6003887b46519deb2afed17281e8 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Wed, 25 Mar 2026 22:26:11 +0200 Subject: [PATCH 03/86] Migrate v1 arguments: strip $ from variable references and convert @N to $N --- scenes/klavier/main.atto | 128 ++++++++++++++++++++++++--------------- src/atto/serial.cpp | 24 +++++++- 2 files changed, 101 insertions(+), 51 deletions(-) diff --git a/scenes/klavier/main.atto b/scenes/klavier/main.atto index bd8eb4f..69ee963 100644 --- a/scenes/klavier/main.atto +++ b/scenes/klavier/main.atto @@ -59,7 +59,7 @@ position = [754.71, 742.669] [[node]] id = "$auto-831e483b4e4602dc" type = "append" -args = ["$oscs"] +args = ["oscs"] inputs = ["$auto-831e483b4e4602dc_s1-out0"] outputs = ["$auto-831e483b4e4602dc-out0"] position = [1762.05, 2099.45] @@ -76,7 +76,7 @@ id = "$auto-c81c38e5f70d7c98" type = "expr" args = ["sin($0.p)*$1/32.f"] inputs = ["$auto-2018c2b6134a0c05-out0", "$auto-2018c2b6134a0c05-out1"] -outputs = ["", "$auto-c81c38e5f70d7c98-post_bang"] +outputs = ["$auto-c81c38e5f70d7c98-out0", "$auto-c81c38e5f70d7c98-post_bang"] position = [1866.25, 1443.11] [[node]] @@ -90,6 +90,7 @@ position = [1784.69, 1675.87] id = "$auto-1d7ed7a2c6bd9465" type = "expr" args = ["0"] +outputs = ["$auto-1d7ed7a2c6bd9465-out0"] position = [1776.07, 1758.84] [[node]] @@ -97,12 +98,14 @@ id = "$auto-cc68627fd36abb94" type = "expr" args = ["2*pi/$0"] inputs = ["$auto-b475716d61845870-out1"] +outputs = ["$auto-cc68627fd36abb94-out0"] position = [1805.57, 1828.22] [[node]] id = "$auto-9786d74433799f1e" type = "expr" args = ["1"] +outputs = ["$auto-9786d74433799f1e-out0"] position = [1831.1, 1894.92] [[node]] @@ -131,6 +134,7 @@ position = [1872.74, 1347.96] id = "$auto-b64eb56b2a60eda2" type = "new" args = ["osc_res"] +outputs = ["", "", "$auto-b64eb56b2a60eda2-as_lambda"] position = [1766.95, 1565.73] [[node]] @@ -138,14 +142,15 @@ id = "$auto-18cbc672fb22e68e" type = "expr" args = ["$0<0.001f"] inputs = ["$auto-2018c2b6134a0c05-out1"] +outputs = ["$auto-18cbc672fb22e68e-out0"] position = [1923.26, 1493.73] [[node]] id = "$auto-e6a647578747ca01" type = "iterate" -args = ["$oscs"] +args = ["oscs"] inputs = ["$auto-a81d5e94c0631e58-as_lambda"] -outputs = ["$auto-e6a647578747ca01-as_lambda"] +outputs = ["", "$auto-e6a647578747ca01-as_lambda"] position = [3718.47, 1973.21] [[node]] @@ -161,7 +166,7 @@ id = "$auto-a81d5e94c0631e58" type = "select" args = ["$0.e", "$2", "$1"] inputs = ["$auto-5551d3ed2caa466f-out0", "$auto-b78d3f85fe8f654a-out0", "$auto-59970d1e2f56ca0f-out0"] -outputs = ["", "$auto-a81d5e94c0631e58-as_lambda"] +outputs = ["", "", "$auto-a81d5e94c0631e58-as_lambda"] position = [3781.35, 1891.39] [[node]] @@ -180,7 +185,7 @@ position = [3811.81, 1541.74] [[node]] id = "$auto-59970d1e2f56ca0f" type = "erase" -args = ["$oscs"] +args = ["oscs"] inputs = ["$auto-59970d1e2f56ca0f_s1-out0"] outputs = ["$auto-59970d1e2f56ca0f-out0"] position = [3839.65, 1835.09] @@ -240,21 +245,21 @@ position = [1106.59, 745.558] [[node]] id = "$auto-0a536bc07ab8e6ba" type = "expr" -args = ["$delay_line_pos", "mod($delay_line_pos+1,$delay_line_size)"] +args = ["delay_line_pos", "mod(delay_line_pos+1,delay_line_size)"] outputs = ["$auto-0a536bc07ab8e6ba-out0", "$auto-0a536bc07ab8e6ba-out1", "$auto-0a536bc07ab8e6ba-post_bang"] position = [3853.11, 2143.2] [[node]] id = "$auto-a34b262f215ea808" type = "store!" -args = ["$delay_line_pos"] +args = ["delay_line_pos"] inputs = ["$auto-0a536bc07ab8e6ba-post_bang", "$auto-0a536bc07ab8e6ba-out1"] position = [4447.05, 2205.49] [[node]] id = "$auto-694aaecf19c1f260" type = "store!" -args = ["$delay_line[$0]"] +args = ["delay_line[$0]"] inputs = ["$auto-a8da10815e6d03ff-bang0", "$auto-0a536bc07ab8e6ba-out0", "$auto-436d853ee8e34fa5-out0"] outputs = ["$auto-694aaecf19c1f260-bang0"] position = [3720.9, 2405.95] @@ -262,7 +267,7 @@ position = [3720.9, 2405.95] [[node]] id = "$auto-436d853ee8e34fa5" type = "expr" -args = ["$0+$delay_line[$1]*0.7f"] +args = ["$0+delay_line[$1]*0.7f"] inputs = ["$auto-e74cec1135c3a130-out0", "$auto-0a536bc07ab8e6ba-out1"] outputs = ["$auto-436d853ee8e34fa5-out0"] position = [3794.02, 2334.26] @@ -291,9 +296,9 @@ position = [771.53, 1196.46] [[node]] id = "$auto-45df349a6ae05f56" type = "store!" -args = ["$delay_line_size"] +args = ["delay_line_size"] inputs = ["$auto-ddaf4497489a54c2-bang0", "$auto-45df349a6ae05f56_s1-out0"] -outputs = ["$step2"] +outputs = ["$auto-45df349a6ae05f56-bang0"] position = [785.327, 1265.73] [[node]] @@ -306,7 +311,7 @@ position = [756.636, 792.818] [[node]] id = "$auto-a8da10815e6d03ff" type = "lock!" -args = ["$oscs_mutex"] +args = ["oscs_mutex"] inputs = ["$auto-e74cec1135c3a130-bang0", "$auto-e6a647578747ca01-as_lambda"] outputs = ["$auto-a8da10815e6d03ff-bang0"] position = [3500.01, 2030.75] @@ -327,7 +332,7 @@ position = [1683.32, 681.704] [[node]] id = "$auto-scan_keys_node" type = "call!" -args = ["$imgui_scan_piano_keys", "$klavie_down", "$klavie_up"] +args = ["imgui_scan_piano_keys", "klavie_down", "klavie_up"] inputs = ["$auto-vtick_void-post_bang"] outputs = ["$auto-scan_keys_node-bang0"] position = [4945.95, 1509.43] @@ -335,7 +340,7 @@ position = [4945.95, 1509.43] [[node]] id = "$auto-4320923f2a319682" type = "call!" -args = ["$imgui_begin_fullscreen"] +args = ["imgui_begin_fullscreen"] inputs = ["$auto-scan_keys_node-bang0"] outputs = ["$auto-4320923f2a319682-bang0"] position = [4945.95, 1579.43] @@ -343,7 +348,7 @@ position = [4945.95, 1579.43] [[node]] id = "$auto-gui_slider_node" type = "call!" -args = ["$imgui_slider_int", "\"Delay Size\"", "$delay_line_size", "1", "48000"] +args = ["imgui_slider_int", "\"Delay Size\"", "delay_line_size", "1", "48000"] inputs = ["$auto-4320923f2a319682-bang0"] outputs = ["$auto-gui_slider_node-bang0"] position = [4945.95, 1652.43] @@ -351,7 +356,7 @@ position = [4945.95, 1652.43] [[node]] id = "$auto-expr_delay_ref" type = "expr" -args = ["$delay_line"] +args = ["delay_line"] outputs = ["$auto-expr_delay_ref-out0"] position = [5122.24, 1727.07] @@ -366,7 +371,7 @@ position = [5041.65, 1803.23] [[node]] id = "$auto-plot_delay_node" type = "call!" -args = ["$imgui_plot_lines_fill", "\"Delay Line\"", "$0", "0", "$delay_line_size", "-1.0f", "1.0f", "\"\""] +args = ["imgui_plot_lines_fill", "\"Delay Line\"", "$0", "0", "delay_line_size", "-1.0f", "1.0f", "\"\""] inputs = ["$auto-gui_slider_node-bang0", "$auto-cast_delay_node-out0"] outputs = ["$auto-plot_delay_node-bang0"] position = [4945.95, 1869.43] @@ -374,7 +379,7 @@ position = [4945.95, 1869.43] [[node]] id = "$auto-gui_end_node" type = "call!" -args = ["$imgui_end"] +args = ["imgui_end"] inputs = ["$auto-plot_delay_node-bang0"] position = [4945.95, 1939.43] @@ -395,9 +400,9 @@ position = [761, 914] [[node]] id = "$auto-daa77173e91ec011" type = "lock" -args = ["$oscs_mutex"] +args = ["oscs_mutex"] inputs = ["$auto-a3cda7b2eaa0cc3c-as_lambda"] -outputs = ["$auto-daa77173e91ec011-as_lambda"] +outputs = ["", "$auto-daa77173e91ec011-as_lambda"] position = [1202.59, 2435.03] [[node]] @@ -410,15 +415,15 @@ position = [1284.42, 1561.27] [[node]] id = "$auto-f79f6fb421f321f0" type = "store!" -args = ["$klavie_down"] -inputs = ["$step2", "$auto-daa77173e91ec011-as_lambda"] +args = ["klavie_down"] +inputs = ["$auto-45df349a6ae05f56-bang0", "$auto-daa77173e91ec011-as_lambda"] outputs = ["$auto-f79f6fb421f321f0-bang0"] -position = [1184.17, 1184.67] +position = [1224.44, 1270.42] [[node]] id = "$auto-487ba455bf42a84c" type = "store!" -args = ["$klavie_up"] +args = ["klavie_up"] inputs = ["$auto-f79f6fb421f321f0-bang0", "$auto-c7381b7375adf0ce-as_lambda"] outputs = ["$auto-487ba455bf42a84c-bang0"] position = [2527.9, 1282.56] @@ -427,13 +432,13 @@ position = [2527.9, 1282.56] id = "$auto-c7381b7375adf0ce" type = "select" inputs = ["$auto-081b2e7c7b05405a-out0", "$auto-a4638623e82d8e17-out0", "$auto-7771f927d195eceb-out0"] -outputs = ["", "$auto-c7381b7375adf0ce-as_lambda"] +outputs = ["", "", "$auto-c7381b7375adf0ce-as_lambda"] position = [2734.55, 1619.72] [[node]] id = "$auto-a4638623e82d8e17" type = "expr" -args = ["$keys[$0].stop($keys[$0])"] +args = ["keys[$0].stop(keys[$0])"] inputs = ["$auto-d08c1c1fdad95ee4-out0"] outputs = ["$auto-a4638623e82d8e17-out0", "$auto-a4638623e82d8e17-post_bang"] position = [2750.22, 1521.28] @@ -441,7 +446,7 @@ position = [2750.22, 1521.28] [[node]] id = "$auto-8bbb6e2930a59a81" type = "erase!" -args = ["$keys"] +args = ["keys"] inputs = ["$auto-a4638623e82d8e17-post_bang", "$auto-d08c1c1fdad95ee4-out0"] position = [3070.95, 1557.71] @@ -455,7 +460,7 @@ position = [2731.42, 1400.77] [[node]] id = "$auto-081b2e7c7b05405a" type = "expr" -args = ["$keys?[$0]"] +args = ["keys?[$0]"] inputs = ["$auto-d08c1c1fdad95ee4-out0"] outputs = ["$auto-081b2e7c7b05405a-out0"] position = [2697.56, 1472.1] @@ -469,15 +474,15 @@ position = [2826.45, 1558.83] [[node]] id = "$auto-a3cda7b2eaa0cc3c" type = "select" -args = ["$keys?[$0]"] +args = ["keys?[$0]"] inputs = ["$auto-b475716d61845870-out0", "$auto-cb9a285934d2d07b-out0", "$auto-225009e132d0e7d5-out0"] -outputs = ["", "$auto-a3cda7b2eaa0cc3c-as_lambda"] +outputs = ["", "$auto-a3cda7b2eaa0cc3c-post_bang", "$auto-a3cda7b2eaa0cc3c-as_lambda"] position = [1297.72, 2337.6] [[node]] id = "$auto-cb9a285934d2d07b" type = "expr" -args = ["$keys[$0].stop($keys[$0])"] +args = ["keys[$0].stop(keys[$0])"] inputs = ["$auto-b475716d61845870-out0"] outputs = ["$auto-cb9a285934d2d07b-out0"] position = [1345.11, 2180.75] @@ -485,8 +490,8 @@ position = [1345.11, 2180.75] [[node]] id = "$auto-182cde3e88fc2aec" type = "store!" -args = ["$keys[$0]"] -inputs = ["", "$auto-b475716d61845870-out0", "$auto-831e483b4e4602dc-out0"] +args = ["keys[$0]"] +inputs = ["$auto-a3cda7b2eaa0cc3c-post_bang", "$auto-b475716d61845870-out0", "$auto-831e483b4e4602dc-out0"] position = [1727.27, 2235.78] [[node]] @@ -518,42 +523,42 @@ position = [766.535, 1028.41] [[node]] id = "$auto-store_atick" type = "store!" -args = ["$audio_tick"] -inputs = ["$auto-487ba455bf42a84c-bang0"] +args = ["audio_tick"] +inputs = ["$auto-487ba455bf42a84c-bang0", "$auto-atick_void-as_lambda"] outputs = ["$auto-store_atick-bang0"] position = [3340.42, 1265.93] [[node]] id = "$auto-atick_void" type = "void" -outputs = ["", "$auto-atick_void-post_bang"] +outputs = ["", "$auto-atick_void-post_bang", "$auto-atick_void-as_lambda"] position = [3496.36, 1376.98] [[node]] id = "$auto-store_vtick" type = "store!" -args = ["$video_tick"] -inputs = ["$auto-store_atick-bang0"] +args = ["video_tick"] +inputs = ["$auto-store_atick-bang0", "$auto-vtick_void-as_lambda"] outputs = ["$auto-store_vtick-bang0"] position = [4623.5, 1281.83] [[node]] id = "$auto-vtick_void" type = "void" -outputs = ["", "$auto-vtick_void-post_bang"] +outputs = ["", "$auto-vtick_void-post_bang", "$auto-vtick_void-as_lambda"] position = [4808.81, 1366.93] [[node]] id = "$auto-av_window_node" type = "call!" -args = ["$av_create_window", "\"Klavier\"", "$audio_tick", "48000", "1", "$video_tick", "800", "600", "$on_quit"] +args = ["av_create_window", "\"Klavier\"", "audio_tick", "48000", "1", "video_tick", "800", "600", "on_quit"] inputs = ["$auto-dcdb8bb52c9d14ef-bang0"] position = [4808.51, 2495.54] [[node]] id = "$auto-dcdb8bb52c9d14ef" type = "store!" -args = ["$on_quit"] +args = ["on_quit"] inputs = ["$auto-store_vtick-bang0", "$auto-56eb5d29abcb9fb0-as_lambda"] outputs = ["$auto-dcdb8bb52c9d14ef-bang0"] position = [4799.81, 2260.41] @@ -568,7 +573,7 @@ position = [766.535, 1080] [[node]] id = "$auto-56eb5d29abcb9fb0" type = "void" -outputs = ["", "$auto-56eb5d29abcb9fb0-as_lambda"] +outputs = ["", "", "$auto-56eb5d29abcb9fb0-as_lambda"] position = [4965.91, 2377.32] [[node]] @@ -642,6 +647,12 @@ type = "decl_var" inputs = ["", "$auto-90dd534c6a74519c-out0", "$auto-1680d2daeb0c8be1-out0"] position = [1832.42, 1132.81] +[[node]] +id = "$auto-123b4f69e60c60d4" +type = "expr" +args = ["f32"] +position = [1555.62, 880.995] + [[node]] id = "$auto-d0fb3eeaa045e214" type = "expr" @@ -675,6 +686,7 @@ id = "$auto-934e3b98bb914e95_s2" type = "expr" shadow = true args = ["e:bool"] +outputs = ["$auto-934e3b98bb914e95_s2-out0"] position = [550.93, 255.328] [[node]] @@ -698,6 +710,7 @@ id = "$auto-e073eb5950485587_s2" type = "expr" shadow = true args = ["->"] +outputs = ["$auto-e073eb5950485587_s2-out0"] position = [551.91, 302.962] [[node]] @@ -705,6 +718,7 @@ id = "$auto-e073eb5950485587_s3" type = "expr" shadow = true args = ["osc_res"] +outputs = ["$auto-e073eb5950485587_s3-out0"] position = [551.91, 242.962] [[node]] @@ -728,6 +742,7 @@ id = "$auto-fe155835bba6cd45_s2" type = "expr" shadow = true args = ["->"] +outputs = ["$auto-fe155835bba6cd45_s2-out0"] position = [554.325, 347.55] [[node]] @@ -735,6 +750,7 @@ id = "$auto-fe155835bba6cd45_s3" type = "expr" shadow = true args = ["void"] +outputs = ["$auto-fe155835bba6cd45_s3-out0"] position = [554.325, 287.55] [[node]] @@ -758,6 +774,7 @@ id = "$auto-09f161f1210cec4f_s2" type = "expr" shadow = true args = ["stop:stop_fn"] +outputs = ["$auto-09f161f1210cec4f_s2-out0"] position = [554.46, 390.991] [[node]] @@ -765,6 +782,7 @@ id = "$auto-09f161f1210cec4f_s3" type = "expr" shadow = true args = ["p:f32"] +outputs = ["$auto-09f161f1210cec4f_s3-out0"] position = [554.46, 330.991] [[node]] @@ -772,6 +790,7 @@ id = "$auto-09f161f1210cec4f_s4" type = "expr" shadow = true args = ["pstep:f32"] +outputs = ["$auto-09f161f1210cec4f_s4-out0"] position = [554.46, 270.991] [[node]] @@ -779,6 +798,7 @@ id = "$auto-09f161f1210cec4f_s5" type = "expr" shadow = true args = ["a:f32"] +outputs = ["$auto-09f161f1210cec4f_s5-out0"] position = [554.46, 210.991] [[node]] @@ -786,6 +806,7 @@ id = "$auto-09f161f1210cec4f_s6" type = "expr" shadow = true args = ["astep:f32"] +outputs = ["$auto-09f161f1210cec4f_s6-out0"] position = [554.46, 150.991] [[node]] @@ -809,6 +830,7 @@ id = "$auto-c0fbc2b794fa65b4_s2" type = "expr" shadow = true args = ["^list_iterator>"] +outputs = ["$auto-c0fbc2b794fa65b4_s2-out0"] position = [554.64, 441.922] [[node]] @@ -944,6 +966,7 @@ id = "$auto-20115e980dcd5b53_s2" type = "expr" shadow = true args = ["->"] +outputs = ["$auto-20115e980dcd5b53_s2-out0"] position = [1467.98, 269.477] [[node]] @@ -951,6 +974,7 @@ id = "$auto-20115e980dcd5b53_s3" type = "expr" shadow = true args = ["void"] +outputs = ["$auto-20115e980dcd5b53_s3-out0"] position = [1467.98, 209.477] [[node]] @@ -998,6 +1022,7 @@ id = "$auto-c5373cf3d77e7979_s2" type = "expr" shadow = true args = ["->"] +outputs = ["$auto-c5373cf3d77e7979_s2-out0"] position = [557.37, 731.95] [[node]] @@ -1005,6 +1030,7 @@ id = "$auto-c5373cf3d77e7979_s3" type = "expr" shadow = true args = ["void"] +outputs = ["$auto-c5373cf3d77e7979_s3-out0"] position = [557.37, 671.95] [[node]] @@ -1028,6 +1054,7 @@ id = "$auto-48a2c13cec7e5013_s2" type = "expr" shadow = true args = ["->"] +outputs = ["$auto-48a2c13cec7e5013_s2-out0"] position = [561, 794] [[node]] @@ -1035,6 +1062,7 @@ id = "$auto-48a2c13cec7e5013_s3" type = "expr" shadow = true args = ["void"] +outputs = ["$auto-48a2c13cec7e5013_s3-out0"] position = [561, 734] [[node]] @@ -1066,6 +1094,7 @@ id = "$auto-c17ebd09a44700e1_s2" type = "expr" shadow = true args = ["->"] +outputs = ["$auto-c17ebd09a44700e1_s2-out0"] position = [563.229, 850.563] [[node]] @@ -1073,6 +1102,7 @@ id = "$auto-c17ebd09a44700e1_s3" type = "expr" shadow = true args = ["void"] +outputs = ["$auto-c17ebd09a44700e1_s3-out0"] position = [563.229, 790.563] [[node]] @@ -1096,6 +1126,7 @@ id = "$auto-50417175624d2751_s2" type = "expr" shadow = true args = ["->"] +outputs = ["$auto-50417175624d2751_s2-out0"] position = [566.535, 908.41] [[node]] @@ -1103,6 +1134,7 @@ id = "$auto-50417175624d2751_s3" type = "expr" shadow = true args = ["void"] +outputs = ["$auto-50417175624d2751_s3-out0"] position = [566.535, 848.41] [[node]] @@ -1126,6 +1158,7 @@ id = "$auto-decl_on_quit_s2" type = "expr" shadow = true args = ["->"] +outputs = ["$auto-decl_on_quit_s2-out0"] position = [566.535, 960] [[node]] @@ -1133,6 +1166,7 @@ id = "$auto-decl_on_quit_s3" type = "expr" shadow = true args = ["void"] +outputs = ["$auto-decl_on_quit_s3-out0"] position = [566.535, 900] [[node]] @@ -1183,7 +1217,7 @@ position = [1749.22, 1038.66] id = "$auto-7837ca36997a9a3d" type = "decl_var" inputs = ["", "$auto-7837ca36997a9a3d_s0-out0", "$auto-7837ca36997a9a3d_s1-out0"] -position = [1412.31, 1178.21] +position = [1419.58, 1130.03] [[node]] id = "$auto-7837ca36997a9a3d_s0" @@ -1249,9 +1283,3 @@ args = ["array"] outputs = ["$auto-7837ca36997a9a3d_s1-out0"] position = [1060.74, 944.346] -[[node]] -id = "$auto-960e1ea09eeca09e" -type = "expr" -args = ["1.0"] -position = [1512.24, 797.574] - diff --git a/src/atto/serial.cpp b/src/atto/serial.cpp index 0d7c126..532d8fd 100644 --- a/src/atto/serial.cpp +++ b/src/atto/serial.cpp @@ -222,7 +222,20 @@ static void resolve_imports(FlowGraph& graph, const std::string& base_path) { } } -// ─── Auto-migrate v1 to v2: assign node_ids and net_names ─── +// ─── Migrate v1 args: strip $ from variable refs, convert @N to $N ─── + +static std::string migrate_args_v1(const std::string& args) { + std::string result; + for (size_t i = 0; i < args.size(); i++) { + if (args[i] == '$' && i + 1 < args.size() && !std::isdigit(args[i + 1])) { + continue; // strip $ from variable names (e.g., $oscs → oscs) + } + result += args[i]; + } + return result; +} + +// ─── Auto-migrate v1 to v2: assign node_ids, net_names, migrate args ─── static void migrate_v1_to_v2(FlowGraph& graph) { // Assign $auto- node IDs to nodes that don't have one @@ -232,6 +245,15 @@ static void migrate_v1_to_v2(FlowGraph& graph) { } } + // Migrate args: strip $ from variable refs, convert @N to $N + for (auto& node : graph.nodes) { + std::string migrated = migrate_args_v1(node.args); + if (migrated != node.args) { + node.args = migrated; + node.parse_args(); + } + } + // Assign net names to links that don't have one // Build unique net names from from_pin int net_counter = 0; From a937964972bacdaa2c35b871af086e9f7daa734e Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Wed, 25 Mar 2026 22:38:41 +0200 Subject: [PATCH 04/86] first steps for graphbuilder --- CMakeLists.txt | 1 + src/atto/graphbuilder.cpp | 110 ++++++++++++++++++++++++++++++++++++++ src/atto/graphbuilder.h | 22 ++++++++ 3 files changed, 133 insertions(+) create mode 100644 src/atto/graphbuilder.cpp create mode 100644 src/atto/graphbuilder.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ff165ef..749ec0e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,7 @@ add_library(attolang STATIC src/atto/graph_index.cpp src/atto/shadow.cpp src/atto/symbol_table.cpp + src/atto/graphbuilder.cpp ) target_include_directories(attolang PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp new file mode 100644 index 0000000..bfa2504 --- /dev/null +++ b/src/atto/graphbuilder.cpp @@ -0,0 +1,110 @@ +#include "graphbuilder.h" +#include "args.h" +#include "expr.h" +#include "node_types.h" +#include "shadow.h" +#include "inference.h" +#include "type_utils.h" + +FlowNode& GraphBuilder::add(const std::string& guid, const std::string& type, const std::string& args, + int num_inputs, int num_outputs) { + auto* nt = find_node_type(type.c_str()); + bool is_expr = type == "expr"; + int di = nt ? nt->inputs : 0; + int nbi = nt ? nt->num_triggers : 0; + int nbo = nt ? nt->num_nexts : 0; + int no = (num_outputs >= 0) ? num_outputs : (nt ? nt->outputs : 1); + + FlowNode node; + node.id = graph.next_node_id(); + node.guid = guid; + node.type_id = node_type_id_from_string(type.c_str()); + node.args = args; + node.position = {0, 0}; + + for (int i = 0; i < nbi; i++) + node.triggers.push_back(make_pin("", "bang_in" + std::to_string(i), "", nullptr, FlowPin::BangTrigger)); + + if (is_expr) { + auto slots = scan_slots(args); + int ni = (num_inputs >= 0) ? num_inputs : slots.total_pin_count(di); + for (int i = 0; i < ni; i++) { + bool il = slots.is_lambda_slot(i); + std::string pn = il ? ("@" + std::to_string(i)) : std::to_string(i); + node.inputs.push_back(make_pin("", pn, "", nullptr, il ? FlowPin::Lambda : FlowPin::Input)); + } + if (!args.empty() && num_outputs < 0) { + auto tokens = tokenize_args(args, false); + no = std::max(1, (int)tokens.size()); + } + } else if (type == "cast" || type == "new") { + int ni = (num_inputs >= 0) ? num_inputs : di; + for (int i = 0; i < ni; i++) { + std::string pn; std::string pt; bool il = false; + if (nt && nt->input_ports && i < nt->inputs) { + pn = nt->input_ports[i].name; + il = (nt->input_ports[i].kind == PortKind::Lambda); + if (nt->input_ports[i].type_name) pt = nt->input_ports[i].type_name; + } else pn = std::to_string(i); + node.inputs.push_back(make_pin("", pn, pt, nullptr, il ? FlowPin::Lambda : FlowPin::Input)); + } + } else { + auto info = compute_inline_args(args, di); + if (!info.error.empty()) node.error = info.error; + int ref_pins = (info.pin_slots.max_slot >= 0) ? (info.pin_slots.max_slot + 1) : 0; + if (num_inputs >= 0) ref_pins = num_inputs; + for (int i = 0; i < ref_pins; i++) { + bool il = info.pin_slots.is_lambda_slot(i); + std::string pn = il ? ("@" + std::to_string(i)) : std::to_string(i); + node.inputs.push_back(make_pin("", pn, "", nullptr, il ? FlowPin::Lambda : FlowPin::Input)); + } + for (int i = info.num_inline_args; i < di; i++) { + std::string pn; std::string pt; bool il = false; + if (nt && nt->input_ports && i < nt->inputs) { + pn = nt->input_ports[i].name; + il = (nt->input_ports[i].kind == PortKind::Lambda); + if (nt->input_ports[i].type_name) pt = nt->input_ports[i].type_name; + } else pn = std::to_string(i); + node.inputs.push_back(make_pin("", pn, pt, nullptr, il ? FlowPin::Lambda : FlowPin::Input)); + } + } + + for (int i = 0; i < no; i++) + node.outputs.push_back(make_pin("", "out" + std::to_string(i), "", nullptr, FlowPin::Output)); + for (int i = 0; i < nbo; i++) { + std::string bname = (nt && nt->next_ports && i < nt->num_nexts) ? nt->next_ports[i].name : ("bang" + std::to_string(i)); + node.nexts.push_back(make_pin("", bname, "", nullptr, FlowPin::BangNext)); + } + + node.rebuild_pin_ids(); + node.parse_args(); + graph.nodes.push_back(std::move(node)); + return graph.nodes.back(); +} + +void GraphBuilder::link(const std::string& from, const std::string& to) { + graph.add_link(from, to); +} + +FlowNode* GraphBuilder::find(const std::string& guid) { + for (auto& n : graph.nodes) if (n.guid == guid) return &n; + return nullptr; +} + +FlowPin* GraphBuilder::find_pin(const std::string& pin_id) { + return graph.find_pin(pin_id); +} + +std::vector GraphBuilder::run_inference() { + resolve_type_based_pins(graph); + generate_shadow_nodes(graph); + GraphInference inference(pool); + return inference.run(graph); +} + +std::vector GraphBuilder::run_full_pipeline() { + resolve_type_based_pins(graph); + generate_shadow_nodes(graph); + GraphInference inference(pool); + return inference.run(graph); +} diff --git a/src/atto/graphbuilder.h b/src/atto/graphbuilder.h new file mode 100644 index 0000000..424c462 --- /dev/null +++ b/src/atto/graphbuilder.h @@ -0,0 +1,22 @@ +#pragma once +#include "model.h" +#include "types.h" +#include +#include + +struct GraphBuilder { + FlowGraph graph; + TypePool pool; + + // Add a node and return reference + FlowNode& add(const std::string& guid, const std::string& type, const std::string& args, + int num_inputs = -1, int num_outputs = -1); + + void link(const std::string& from, const std::string& to); + + FlowNode* find(const std::string& guid); + FlowPin* find_pin(const std::string& pin_id); + + std::vector run_inference(); + std::vector run_full_pipeline(); +}; From b1dea6f56942325fce4a101b1ea32eddac8e3ccb Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Wed, 25 Mar 2026 23:04:23 +0200 Subject: [PATCH 05/86] argument parsing changes: add split_args and parse_args_v2 functions, update GraphBuilder to handle parsed arguments --- src/atto/args.cpp | 137 ++++++++++++++++++++++++++++++++++++++ src/atto/args.h | 16 ++++- src/atto/graphbuilder.cpp | 100 ++++++++++++++++++++++------ src/atto/graphbuilder.h | 22 +++++- src/atto/node_types.h | 2 + 5 files changed, 254 insertions(+), 23 deletions(-) diff --git a/src/atto/args.cpp b/src/atto/args.cpp index 34a403d..ad318e7 100644 --- a/src/atto/args.cpp +++ b/src/atto/args.cpp @@ -264,3 +264,140 @@ void FlowNode::parse_args() { inline_meta.ref_pin_count = (info.pin_slots.max_slot >= 0) ? (info.pin_slots.max_slot + 1) : 0; } } + +// ─── split_args: split string into singular expressions ─── + +SplitResult split_args(const std::string& args_str) { + std::vector result; + std::string current; + int paren_depth = 0; + int brace_depth = 0; + bool in_string = false; + bool escape = false; + + for (size_t i = 0; i < args_str.size(); i++) { + char c = args_str[i]; + + if (escape) { + current += c; + escape = false; + continue; + } + if (c == '\\' && in_string) { + escape = true; + current += c; + continue; + } + if (c == '"') { + in_string = !in_string; + current += c; + continue; + } + if (in_string) { + current += c; + continue; + } + + if (c == '(') { paren_depth++; current += c; continue; } + if (c == ')') { + paren_depth--; + if (paren_depth < 0) + return std::string("Mismatched ')' at position " + std::to_string(i)); + current += c; + continue; + } + if (c == '{') { brace_depth++; current += c; continue; } + if (c == '}') { + brace_depth--; + if (brace_depth < 0) + return std::string("Mismatched '}' at position " + std::to_string(i)); + current += c; + continue; + } + + if ((c == ' ' || c == '\t') && paren_depth == 0 && brace_depth == 0) { + if (!current.empty()) { + result.push_back(current); + current.clear(); + } + continue; + } + + current += c; + } + + if (in_string) + return std::string("Unterminated string literal"); + if (paren_depth > 0) + return std::string("Unclosed '(' — " + std::to_string(paren_depth) + " level(s) deep"); + if (brace_depth > 0) + return std::string("Unclosed '{' — " + std::to_string(brace_depth) + " level(s) deep"); + + if (!current.empty()) + result.push_back(current); + + return result; +} + +// ─── parse_args_v2: parse pre-split expressions ─── + +ParseResult parse_args_v2(const std::vector& exprs, bool is_expr) { + auto result = std::make_unique(); + + auto register_slot = [&](int index, bool is_lambda) { + result->slots[index] = is_lambda; + result->max_slot = std::max(result->max_slot, index); + }; + + // Scan all expressions for $N and @N refs + for (auto& expr : exprs) { + for (size_t i = 0; i < expr.size(); i++) { + if ((expr[i] == '$' || expr[i] == '@') && i + 1 < expr.size() && + expr[i + 1] >= '0' && expr[i + 1] <= '9') { + bool is_lambda = (expr[i] == '@'); + int n = 0; + size_t j = i + 1; + while (j < expr.size() && expr[j] >= '0' && expr[j] <= '9') { + n = n * 10 + (expr[j] - '0'); + j++; + } + register_slot(n, is_lambda); + } + } + } + + for (auto& expr : exprs) { + result->args.push_back(parse_token(expr)); + } + result->has_any_args = !exprs.empty(); + + return result; +} + +// ─── reconstruct_args_str ─── + +std::string reconstruct_args_str(const ParsedArgs& args) { + std::string result; + for (auto& a : args.args) { + if (!result.empty()) result += " "; + std::visit([&](auto& v) { + using T = std::decay_t; + if constexpr (std::is_same_v) result += "$" + std::to_string(v.index); + else if constexpr (std::is_same_v) result += "@" + std::to_string(v.index); + else if constexpr (std::is_same_v) result += v.name; + else if constexpr (std::is_same_v) result += "#" + v.enum_name + "." + v.value_name; + else if constexpr (std::is_same_v) { + if (v.is_float) { + char buf[64]; + snprintf(buf, sizeof(buf), "%g", v.value); + result += buf; + } else { + result += std::to_string((long long)v.value); + } + } + else if constexpr (std::is_same_v) result += "\"" + v.value + "\""; + else if constexpr (std::is_same_v) result += v.expr; + }, a); + } + return result; +} diff --git a/src/atto/args.h b/src/atto/args.h index d15a26a..e5bb76b 100644 --- a/src/atto/args.h +++ b/src/atto/args.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -68,5 +69,18 @@ int find_max_port_ref(const std::string& s); // Parse a single token into a FlowArg FlowArg parse_token(const std::string& tok); -// Parse a full argument string +// Parse a full argument string (legacy wrapper) ParsedArgs parse_args(const std::string& args_str, bool is_expr = false); + +// Split an args string into singular expressions (space-delimited, aware of () {} "" nesting). +// Returns vector on success, or error string on failure (mismatched parens/braces/quotes). +using SplitResult = std::variant, std::string>; +SplitResult split_args(const std::string& args_str); + +// Parse pre-split expressions into ParsedArgs. +// Returns unique_ptr on success, or error string on failure. +using ParseResult = std::variant, std::string>; +ParseResult parse_args_v2(const std::vector& exprs, bool is_expr = false); + +// Reconstruct a space-separated args string from a ParsedArgs +std::string reconstruct_args_str(const ParsedArgs& args); diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index bfa2504..5545723 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -5,39 +5,47 @@ #include "shadow.h" #include "inference.h" #include "type_utils.h" +#include -FlowNode& GraphBuilder::add(const std::string& guid, const std::string& type, const std::string& args, +// ─── GraphBuilder ─── + +FlowNode& GraphBuilder::add(const std::string& id, NodeTypeID type, std::unique_ptr parsed_args, int num_inputs, int num_outputs) { - auto* nt = find_node_type(type.c_str()); - bool is_expr = type == "expr"; + if (!parsed_args) + throw std::invalid_argument("GraphBuilder::add: parsed_args must not be null"); + + auto* nt = find_node_type(type); + bool is_expr = is_any_of(type, NodeTypeID::Expr, NodeTypeID::ExprBang); int di = nt ? nt->inputs : 0; int nbi = nt ? nt->num_triggers : 0; int nbo = nt ? nt->num_nexts : 0; int no = (num_outputs >= 0) ? num_outputs : (nt ? nt->outputs : 1); + std::string args_str = reconstruct_args_str(*parsed_args); + FlowNode node; node.id = graph.next_node_id(); - node.guid = guid; - node.type_id = node_type_id_from_string(type.c_str()); - node.args = args; + node.node_id = id; + node.guid = (id.size() > 1 && id[0] == '$') ? id.substr(1) : id; + node.type_id = type; + node.args = args_str; node.position = {0, 0}; for (int i = 0; i < nbi; i++) node.triggers.push_back(make_pin("", "bang_in" + std::to_string(i), "", nullptr, FlowPin::BangTrigger)); if (is_expr) { - auto slots = scan_slots(args); - int ni = (num_inputs >= 0) ? num_inputs : slots.total_pin_count(di); + int ni = (num_inputs >= 0) ? num_inputs : parsed_args->total_pin_count(di); for (int i = 0; i < ni; i++) { - bool il = slots.is_lambda_slot(i); + bool il = parsed_args->is_lambda_slot(i); std::string pn = il ? ("@" + std::to_string(i)) : std::to_string(i); node.inputs.push_back(make_pin("", pn, "", nullptr, il ? FlowPin::Lambda : FlowPin::Input)); } - if (!args.empty() && num_outputs < 0) { - auto tokens = tokenize_args(args, false); + if (!args_str.empty() && num_outputs < 0) { + auto tokens = tokenize_args(args_str, false); no = std::max(1, (int)tokens.size()); } - } else if (type == "cast" || type == "new") { + } else if (is_any_of(type, NodeTypeID::Cast, NodeTypeID::New)) { int ni = (num_inputs >= 0) ? num_inputs : di; for (int i = 0; i < ni; i++) { std::string pn; std::string pt; bool il = false; @@ -49,16 +57,15 @@ FlowNode& GraphBuilder::add(const std::string& guid, const std::string& type, co node.inputs.push_back(make_pin("", pn, pt, nullptr, il ? FlowPin::Lambda : FlowPin::Input)); } } else { - auto info = compute_inline_args(args, di); - if (!info.error.empty()) node.error = info.error; - int ref_pins = (info.pin_slots.max_slot >= 0) ? (info.pin_slots.max_slot + 1) : 0; + int ref_pins = (parsed_args->max_slot >= 0) ? (parsed_args->max_slot + 1) : 0; if (num_inputs >= 0) ref_pins = num_inputs; for (int i = 0; i < ref_pins; i++) { - bool il = info.pin_slots.is_lambda_slot(i); + bool il = parsed_args->is_lambda_slot(i); std::string pn = il ? ("@" + std::to_string(i)) : std::to_string(i); node.inputs.push_back(make_pin("", pn, "", nullptr, il ? FlowPin::Lambda : FlowPin::Input)); } - for (int i = info.num_inline_args; i < di; i++) { + int num_inline = (int)parsed_args->args.size(); + for (int i = num_inline; i < di; i++) { std::string pn; std::string pt; bool il = false; if (nt && nt->input_ports && i < nt->inputs) { pn = nt->input_ports[i].name; @@ -86,8 +93,8 @@ void GraphBuilder::link(const std::string& from, const std::string& to) { graph.add_link(from, to); } -FlowNode* GraphBuilder::find(const std::string& guid) { - for (auto& n : graph.nodes) if (n.guid == guid) return &n; +FlowNode* GraphBuilder::find(const std::string& id) { + for (auto& n : graph.nodes) if (n.guid == id || n.node_id == id) return &n; return nullptr; } @@ -108,3 +115,58 @@ std::vector GraphBuilder::run_full_pipeline() { GraphInference inference(pool); return inference.run(graph); } + +// ─── Deserializer ─── + +static FlowNode& make_error_node(FlowGraph& graph, const std::string& id, + const std::string& type, const std::string& args_str, + const std::string& error_msg) { + FlowNode node; + node.id = graph.next_node_id(); + node.node_id = id; + node.guid = (id.size() > 1 && id[0] == '$') ? id.substr(1) : id; + node.type_id = NodeTypeID::Error; + node.args = type + " " + args_str; + node.error = error_msg; + node.position = {0, 0}; + node.rebuild_pin_ids(); + graph.nodes.push_back(std::move(node)); + return graph.nodes.back(); +} + +FlowNode& Deserializer::add(const std::string& id, const std::string& type, const std::string& args_str, + int num_inputs, int num_outputs) { + NodeTypeID type_id = node_type_id_from_string(type.c_str()); + + if (type_id == NodeTypeID::Unknown) { + return make_error_node(builder->graph, id, type, args_str, "Unknown node type: " + type); + } + + // Labels and errors don't need parsing + if (is_any_of(type_id, NodeTypeID::Label, NodeTypeID::Error)) { + auto parsed = std::make_unique(); + if (!args_str.empty()) { + parsed->args.push_back(ArgString{args_str}); + parsed->has_any_args = true; + } + return builder->add(id, type_id, std::move(parsed), num_inputs, num_outputs); + } + + // Split args + auto split_result = split_args(args_str); + if (auto* err = std::get_if(&split_result)) { + return make_error_node(builder->graph, id, type, args_str, *err); + } + + auto& exprs = std::get>(split_result); + bool is_expr = is_any_of(type_id, NodeTypeID::Expr, NodeTypeID::ExprBang); + + // Parse args + auto parse_result = parse_args_v2(exprs, is_expr); + if (auto* err = std::get_if(&parse_result)) { + return make_error_node(builder->graph, id, type, args_str, *err); + } + + auto parsed = std::get>(std::move(parse_result)); + return builder->add(id, type_id, std::move(parsed), num_inputs, num_outputs); +} diff --git a/src/atto/graphbuilder.h b/src/atto/graphbuilder.h index 424c462..791c428 100644 --- a/src/atto/graphbuilder.h +++ b/src/atto/graphbuilder.h @@ -1,22 +1,38 @@ #pragma once #include "model.h" #include "types.h" +#include "args.h" +#include "node_types.h" #include #include +#include +#include struct GraphBuilder { FlowGraph graph; TypePool pool; - // Add a node and return reference - FlowNode& add(const std::string& guid, const std::string& type, const std::string& args, + // Primary add: takes pre-parsed args (must not be null — throws on null) + FlowNode& add(const std::string& id, NodeTypeID type, std::unique_ptr args, int num_inputs = -1, int num_outputs = -1); void link(const std::string& from, const std::string& to); - FlowNode* find(const std::string& guid); + FlowNode* find(const std::string& id); FlowPin* find_pin(const std::string& pin_id); std::vector run_inference(); std::vector run_full_pipeline(); }; + +// Deserializer: string-based node creation with error handling. +// Wraps GraphBuilder, handles split/parse/error-fallback. +struct Deserializer { + GraphBuilder* builder; + + explicit Deserializer(GraphBuilder* b) : builder(b) {} + + // Add a node from raw strings. On parse failure, creates an Error node. + FlowNode& add(const std::string& id, const std::string& type, const std::string& args_str, + int num_inputs = -1, int num_outputs = -1); +}; diff --git a/src/atto/node_types.h b/src/atto/node_types.h index 1346ebc..7627411 100644 --- a/src/atto/node_types.h +++ b/src/atto/node_types.h @@ -41,6 +41,7 @@ enum class NodeTypeID : uint8_t { Cast, // 34 Label, // 35 Deref, // 36 — internal: dereference iterator to value (shadow node only) + Error, // 37 — error node: displays original args, no pins (like label) COUNT, Unknown = 255 }; @@ -141,6 +142,7 @@ static const NodeType NODE_TYPES[] = { {NodeTypeID::Cast, "cast", "Cast value to type", 0,1, 0,1, false,false,false,false, nullptr, P_VALUE, nullptr, P_RESULT}, {NodeTypeID::Label, "label", "Text label (no connections)", 0,0, 0,0, false,true, false,false, nullptr, nullptr, nullptr, nullptr}, {NodeTypeID::Deref, "deref", "Dereference iterator (internal)", 0,1, 0,1, false,false,false,false, nullptr, P_VALUE, nullptr, P_RESULT}, + {NodeTypeID::Error, "error", "Error: invalid node", 0,0, 0,0, false,false,false,false, nullptr, nullptr, nullptr, nullptr}, }; static constexpr int NUM_NODE_TYPES = sizeof(NODE_TYPES) / sizeof(NODE_TYPES[0]); From 9ff34c6b7ae9c7fd4fe2311c8610880a9f6bd6e1 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Wed, 25 Mar 2026 23:20:28 +0200 Subject: [PATCH 06/86] Initials steps for graphbuilder --- src/atto/graphbuilder.cpp | 179 +++++++++++--------------------------- src/atto/graphbuilder.h | 43 +++++---- 2 files changed, 73 insertions(+), 149 deletions(-) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index 5545723..b36a987 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -2,171 +2,90 @@ #include "args.h" #include "expr.h" #include "node_types.h" -#include "shadow.h" -#include "inference.h" -#include "type_utils.h" -#include -// ─── GraphBuilder ─── +// ─── FlowNodeBuilder ─── -FlowNode& GraphBuilder::add(const std::string& id, NodeTypeID type, std::unique_ptr parsed_args, - int num_inputs, int num_outputs) { - if (!parsed_args) - throw std::invalid_argument("GraphBuilder::add: parsed_args must not be null"); - - auto* nt = find_node_type(type); - bool is_expr = is_any_of(type, NodeTypeID::Expr, NodeTypeID::ExprBang); - int di = nt ? nt->inputs : 0; - int nbi = nt ? nt->num_triggers : 0; - int nbo = nt ? nt->num_nexts : 0; - int no = (num_outputs >= 0) ? num_outputs : (nt ? nt->outputs : 1); - - std::string args_str = reconstruct_args_str(*parsed_args); - - FlowNode node; - node.id = graph.next_node_id(); - node.node_id = id; - node.guid = (id.size() > 1 && id[0] == '$') ? id.substr(1) : id; - node.type_id = type; - node.args = args_str; - node.position = {0, 0}; - - for (int i = 0; i < nbi; i++) - node.triggers.push_back(make_pin("", "bang_in" + std::to_string(i), "", nullptr, FlowPin::BangTrigger)); - - if (is_expr) { - int ni = (num_inputs >= 0) ? num_inputs : parsed_args->total_pin_count(di); - for (int i = 0; i < ni; i++) { - bool il = parsed_args->is_lambda_slot(i); - std::string pn = il ? ("@" + std::to_string(i)) : std::to_string(i); - node.inputs.push_back(make_pin("", pn, "", nullptr, il ? FlowPin::Lambda : FlowPin::Input)); - } - if (!args_str.empty() && num_outputs < 0) { - auto tokens = tokenize_args(args_str, false); - no = std::max(1, (int)tokens.size()); - } - } else if (is_any_of(type, NodeTypeID::Cast, NodeTypeID::New)) { - int ni = (num_inputs >= 0) ? num_inputs : di; - for (int i = 0; i < ni; i++) { - std::string pn; std::string pt; bool il = false; - if (nt && nt->input_ports && i < nt->inputs) { - pn = nt->input_ports[i].name; - il = (nt->input_ports[i].kind == PortKind::Lambda); - if (nt->input_ports[i].type_name) pt = nt->input_ports[i].type_name; - } else pn = std::to_string(i); - node.inputs.push_back(make_pin("", pn, pt, nullptr, il ? FlowPin::Lambda : FlowPin::Input)); - } - } else { - int ref_pins = (parsed_args->max_slot >= 0) ? (parsed_args->max_slot + 1) : 0; - if (num_inputs >= 0) ref_pins = num_inputs; - for (int i = 0; i < ref_pins; i++) { - bool il = parsed_args->is_lambda_slot(i); - std::string pn = il ? ("@" + std::to_string(i)) : std::to_string(i); - node.inputs.push_back(make_pin("", pn, "", nullptr, il ? FlowPin::Lambda : FlowPin::Input)); - } - int num_inline = (int)parsed_args->args.size(); - for (int i = num_inline; i < di; i++) { - std::string pn; std::string pt; bool il = false; - if (nt && nt->input_ports && i < nt->inputs) { - pn = nt->input_ports[i].name; - il = (nt->input_ports[i].kind == PortKind::Lambda); - if (nt->input_ports[i].type_name) pt = nt->input_ports[i].type_name; - } else pn = std::to_string(i); - node.inputs.push_back(make_pin("", pn, pt, nullptr, il ? FlowPin::Lambda : FlowPin::Input)); - } - } +std::string FlowNodeBuilder::args_str() const { + if (!parsed_args) return ""; + return reconstruct_args_str(*parsed_args); +} - for (int i = 0; i < no; i++) - node.outputs.push_back(make_pin("", "out" + std::to_string(i), "", nullptr, FlowPin::Output)); - for (int i = 0; i < nbo; i++) { - std::string bname = (nt && nt->next_ports && i < nt->num_nexts) ? nt->next_ports[i].name : ("bang" + std::to_string(i)); - node.nexts.push_back(make_pin("", bname, "", nullptr, FlowPin::BangNext)); - } +// ─── GraphBuilder ─── - node.rebuild_pin_ids(); - node.parse_args(); - graph.nodes.push_back(std::move(node)); - return graph.nodes.back(); +std::shared_ptr GraphBuilder::add(NodeId id, NodeTypeID type, std::shared_ptr args) { + auto nb = std::make_shared(); + nb->id = std::move(id); + nb->type_id = type; + nb->parsed_args = std::move(args); + builders.push_back(nb); + return nb; } void GraphBuilder::link(const std::string& from, const std::string& to) { - graph.add_link(from, to); + // TODO: links between FlowNodeBuilders } -FlowNode* GraphBuilder::find(const std::string& id) { - for (auto& n : graph.nodes) if (n.guid == id || n.node_id == id) return &n; +std::shared_ptr GraphBuilder::find(const NodeId& id) { + for (auto& nb : builders) if (nb->id == id) return nb; return nullptr; } -FlowPin* GraphBuilder::find_pin(const std::string& pin_id) { - return graph.find_pin(pin_id); -} - -std::vector GraphBuilder::run_inference() { - resolve_type_based_pins(graph); - generate_shadow_nodes(graph); - GraphInference inference(pool); - return inference.run(graph); -} - -std::vector GraphBuilder::run_full_pipeline() { - resolve_type_based_pins(graph); - generate_shadow_nodes(graph); - GraphInference inference(pool); - return inference.run(graph); -} - // ─── Deserializer ─── -static FlowNode& make_error_node(FlowGraph& graph, const std::string& id, - const std::string& type, const std::string& args_str, - const std::string& error_msg) { - FlowNode node; - node.id = graph.next_node_id(); - node.node_id = id; - node.guid = (id.size() > 1 && id[0] == '$') ? id.substr(1) : id; - node.type_id = NodeTypeID::Error; - node.args = type + " " + args_str; - node.error = error_msg; - node.position = {0, 0}; - node.rebuild_pin_ids(); - graph.nodes.push_back(std::move(node)); - return graph.nodes.back(); +static std::shared_ptr make_error(const NodeId& id, const std::string& type, + const std::string& args_str, const std::string& error_msg) { + auto nb = std::make_shared(); + nb->id = id; + nb->type_id = NodeTypeID::Error; + nb->parsed_args = std::make_shared(); + nb->parsed_args->args.push_back(ArgString{type + " " + args_str}); + nb->parsed_args->has_any_args = true; + nb->error = error_msg; + return nb; } -FlowNode& Deserializer::add(const std::string& id, const std::string& type, const std::string& args_str, - int num_inputs, int num_outputs) { +std::shared_ptr Deserializer::parse_node( + const std::shared_ptr& gb, + const NodeId& id, const std::string& type, const std::string& args_str) { + NodeTypeID type_id = node_type_id_from_string(type.c_str()); if (type_id == NodeTypeID::Unknown) { - return make_error_node(builder->graph, id, type, args_str, "Unknown node type: " + type); + auto nb = make_error(id, type, args_str, "Unknown node type: " + type); + gb->builders.push_back(nb); + return nb; } - // Labels and errors don't need parsing + // Labels and errors: no expression parsing if (is_any_of(type_id, NodeTypeID::Label, NodeTypeID::Error)) { - auto parsed = std::make_unique(); + auto pa = std::make_shared(); if (!args_str.empty()) { - parsed->args.push_back(ArgString{args_str}); - parsed->has_any_args = true; + pa->args.push_back(ArgString{args_str}); + pa->has_any_args = true; } - return builder->add(id, type_id, std::move(parsed), num_inputs, num_outputs); + return gb->add(id, type_id, std::move(pa)); } - // Split args + // Split args into expressions auto split_result = split_args(args_str); if (auto* err = std::get_if(&split_result)) { - return make_error_node(builder->graph, id, type, args_str, *err); + auto nb = make_error(id, type, args_str, *err); + gb->builders.push_back(nb); + return nb; } auto& exprs = std::get>(split_result); bool is_expr = is_any_of(type_id, NodeTypeID::Expr, NodeTypeID::ExprBang); - // Parse args + // Parse expressions auto parse_result = parse_args_v2(exprs, is_expr); if (auto* err = std::get_if(&parse_result)) { - return make_error_node(builder->graph, id, type, args_str, *err); + auto nb = make_error(id, type, args_str, *err); + gb->builders.push_back(nb); + return nb; } - auto parsed = std::get>(std::move(parse_result)); - return builder->add(id, type_id, std::move(parsed), num_inputs, num_outputs); + auto parsed = std::shared_ptr( + std::get>(std::move(parse_result)).release()); + return gb->add(id, type_id, std::move(parsed)); } diff --git a/src/atto/graphbuilder.h b/src/atto/graphbuilder.h index 791c428..1a36500 100644 --- a/src/atto/graphbuilder.h +++ b/src/atto/graphbuilder.h @@ -6,33 +6,38 @@ #include #include #include -#include + +using NodeId = std::string; + +// A node under construction — holds structured parsed args instead of raw string. +struct FlowNodeBuilder { + NodeId id; + NodeTypeID type_id = NodeTypeID::Unknown; + std::shared_ptr parsed_args; + Vec2 position = {0, 0}; + bool shadow = false; + std::string error; + + // Reconstruct args string (for legacy code) + std::string args_str() const; +}; struct GraphBuilder { - FlowGraph graph; TypePool pool; + std::vector> builders; - // Primary add: takes pre-parsed args (must not be null — throws on null) - FlowNode& add(const std::string& id, NodeTypeID type, std::unique_ptr args, - int num_inputs = -1, int num_outputs = -1); + // Add a pre-built node + std::shared_ptr add(NodeId id, NodeTypeID type, std::shared_ptr args); void link(const std::string& from, const std::string& to); - FlowNode* find(const std::string& id); - FlowPin* find_pin(const std::string& pin_id); - - std::vector run_inference(); - std::vector run_full_pipeline(); + std::shared_ptr find(const NodeId& id); }; -// Deserializer: string-based node creation with error handling. -// Wraps GraphBuilder, handles split/parse/error-fallback. +// Deserializer: parses raw strings into FlowNodeBuilder, with error fallback. struct Deserializer { - GraphBuilder* builder; - - explicit Deserializer(GraphBuilder* b) : builder(b) {} - - // Add a node from raw strings. On parse failure, creates an Error node. - FlowNode& add(const std::string& id, const std::string& type, const std::string& args_str, - int num_inputs = -1, int num_outputs = -1); + // Parse a node from raw strings. On failure, returns an Error node builder. + static std::shared_ptr parse_node( + const std::shared_ptr& gb, + const NodeId& id, const std::string& type, const std::string& args_str); }; From 4772975bafade99ad54f9fac3bbfc58a22aa9343 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Wed, 25 Mar 2026 23:40:38 +0200 Subject: [PATCH 07/86] more graphbuilder work --- src/atto/graphbuilder.cpp | 224 +++++++++++++++++++++++++++++++------- src/atto/graphbuilder.h | 19 +++- 2 files changed, 202 insertions(+), 41 deletions(-) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index b36a987..19f4f9c 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -2,6 +2,59 @@ #include "args.h" #include "expr.h" #include "node_types.h" +#include + +// ─── TOML helpers (shared with serial.cpp) ─── + +static std::string trim(std::string s) { + while (!s.empty() && (s.front() == ' ' || s.front() == '\t')) s.erase(s.begin()); + while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) s.pop_back(); + return s; +} + +static std::string unescape_toml(const std::string& s) { + std::string result; + result.reserve(s.size()); + for (size_t i = 0; i < s.size(); i++) { + if (s[i] == '\\' && i + 1 < s.size()) { + switch (s[i + 1]) { + case '"': result += '"'; i++; break; + case '\\': result += '\\'; i++; break; + case 'n': result += '\n'; i++; break; + case 't': result += '\t'; i++; break; + case 'r': result += '\r'; i++; break; + default: result += s[i]; break; + } + } else { + result += s[i]; + } + } + return result; +} + +static std::string unquote(const std::string& s) { + if (s.size() >= 2 && s.front() == '"' && s.back() == '"') + return unescape_toml(s.substr(1, s.size() - 2)); + return s; +} + +static std::vector parse_toml_array(const std::string& val) { + std::vector result; + std::string s = trim(val); + if (s.empty() || s.front() != '[' || s.back() != ']') return result; + s = s.substr(1, s.size() - 2); + std::string item; + bool in_str = false, escaped = false; + for (char c : s) { + if (escaped) { item += c; escaped = false; continue; } + if (c == '\\' && in_str) { item += c; escaped = true; continue; } + if (c == '"') { in_str = !in_str; item += c; continue; } + if (c == ',' && !in_str) { result.push_back(unquote(trim(item))); item.clear(); continue; } + item += c; + } + if (!trim(item).empty()) result.push_back(unquote(trim(item))); + return result; +} // ─── FlowNodeBuilder ─── @@ -32,60 +85,155 @@ std::shared_ptr GraphBuilder::find(const NodeId& id) { // ─── Deserializer ─── -static std::shared_ptr make_error(const NodeId& id, const std::string& type, - const std::string& args_str, const std::string& error_msg) { +BuilderResult Deserializer::parse_node( + const NodeId& id, const std::string& type, const std::vector& args) { + + NodeTypeID type_id = node_type_id_from_string(type.c_str()); + + if (type_id == NodeTypeID::Unknown) { + return BuilderError("Unknown node type: " + type); + } + + // Labels and errors: exactly 1 ArgString + if (is_any_of(type_id, NodeTypeID::Label, NodeTypeID::Error)) { + if (args.size() != 1) + throw std::invalid_argument("Label/Error node requires exactly 1 argument, got " + std::to_string(args.size())); + auto nb = std::make_shared(); + nb->id = id; + nb->type_id = type_id; + nb->parsed_args = std::make_shared(); + nb->parsed_args->args.push_back(ArgString{args[0]}); + nb->parsed_args->has_any_args = true; + return nb; + } + + bool is_expr = is_any_of(type_id, NodeTypeID::Expr, NodeTypeID::ExprBang); + + // Parse expressions (args are already split) + auto parse_result = parse_args_v2(args, is_expr); + if (auto* err = std::get_if(&parse_result)) { + return BuilderError(*err); + } + + auto parsed = std::shared_ptr( + std::get>(std::move(parse_result)).release()); + + auto nb = std::make_shared(); + nb->id = id; + nb->type_id = type_id; + nb->parsed_args = std::move(parsed); + return nb; +} + +std::shared_ptr Deserializer::parse_or_error( + const std::shared_ptr& gb, + const NodeId& id, const std::string& type, const std::vector& args) { + + auto result = parse_node(id, type, args); + + if (auto* nb = std::get_if>(&result)) { + gb->builders.push_back(*nb); + return *nb; + } + + auto& error_msg = std::get(result); + // Reconstruct original args for error display + std::string args_joined; + for (auto& a : args) { + if (!args_joined.empty()) args_joined += " "; + args_joined += a; + } auto nb = std::make_shared(); nb->id = id; nb->type_id = NodeTypeID::Error; nb->parsed_args = std::make_shared(); - nb->parsed_args->args.push_back(ArgString{type + " " + args_str}); + nb->parsed_args->args.push_back(ArgString{type + " " + args_joined}); nb->parsed_args->has_any_args = true; nb->error = error_msg; + gb->builders.push_back(nb); return nb; } -std::shared_ptr Deserializer::parse_node( - const std::shared_ptr& gb, - const NodeId& id, const std::string& type, const std::string& args_str) { - - NodeTypeID type_id = node_type_id_from_string(type.c_str()); +// ─── parse_atto: instrument@atto:0 stream parser ─── - if (type_id == NodeTypeID::Unknown) { - auto nb = make_error(id, type, args_str, "Unknown node type: " + type); - gb->builders.push_back(nb); - return nb; +Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { + // Check version header + std::string first_line; + while (std::getline(f, first_line)) { + first_line = trim(first_line); + if (!first_line.empty()) break; + } + if (first_line != "# version instrument@atto:0") { + return BuilderError("Expected '# version instrument@atto:0', got: " + first_line); } - // Labels and errors: no expression parsing - if (is_any_of(type_id, NodeTypeID::Label, NodeTypeID::Error)) { - auto pa = std::make_shared(); - if (!args_str.empty()) { - pa->args.push_back(ArgString{args_str}); - pa->has_any_args = true; + auto gb = std::make_shared(); + + // Parse state + bool in_node = false; + std::string cur_id, cur_type; + std::vector cur_args; + std::vector cur_inputs, cur_outputs; + float cur_x = 0, cur_y = 0; + bool cur_shadow = false; + + auto flush_node = [&]() { + if (cur_type.empty()) { + cur_id.clear(); cur_args.clear(); cur_inputs.clear(); cur_outputs.clear(); + return; } - return gb->add(id, type_id, std::move(pa)); - } - // Split args into expressions - auto split_result = split_args(args_str); - if (auto* err = std::get_if(&split_result)) { - auto nb = make_error(id, type, args_str, *err); - gb->builders.push_back(nb); - return nb; - } + if (cur_id.empty()) { + cur_id = "$auto-" + generate_guid(); + } - auto& exprs = std::get>(split_result); - bool is_expr = is_any_of(type_id, NodeTypeID::Expr, NodeTypeID::ExprBang); + auto nb = parse_or_error(gb, cur_id, cur_type, cur_args); + nb->position = {cur_x, cur_y}; + nb->shadow = cur_shadow; - // Parse expressions - auto parse_result = parse_args_v2(exprs, is_expr); - if (auto* err = std::get_if(&parse_result)) { - auto nb = make_error(id, type, args_str, *err); - gb->builders.push_back(nb); - return nb; + // Store inputs/outputs net refs on the builder for later resolution + // TODO: resolve net connections after all nodes are parsed + + cur_id.clear(); cur_type.clear(); cur_args.clear(); + cur_inputs.clear(); cur_outputs.clear(); + cur_x = 0; cur_y = 0; cur_shadow = false; + }; + + std::string line; + while (std::getline(f, line)) { + line = trim(line); + if (line.empty() || (line[0] == '#' && line.find("# version") != 0)) continue; + + if (line == "[[node]]") { + flush_node(); + in_node = true; + continue; + } + + if (line.find("# version") == 0) continue; + + if (!in_node) continue; + + auto eq = line.find('='); + if (eq == std::string::npos) continue; + std::string key = trim(line.substr(0, eq)); + std::string val = trim(line.substr(eq + 1)); + + if (key == "id") { cur_id = unquote(val); } + else if (key == "type") { cur_type = unquote(val); } + else if (key == "args") { cur_args = parse_toml_array(val); } + else if (key == "shadow") { cur_shadow = (unquote(val) == "true"); } + else if (key == "inputs") { cur_inputs = parse_toml_array(val); } + else if (key == "outputs") { cur_outputs = parse_toml_array(val); } + else if (key == "position") { + auto coords = parse_toml_array(val); + if (coords.size() >= 2) { + cur_x = std::stof(coords[0]); + cur_y = std::stof(coords[1]); + } + } } + flush_node(); - auto parsed = std::shared_ptr( - std::get>(std::move(parse_result)).release()); - return gb->add(id, type_id, std::move(parsed)); + return gb; } diff --git a/src/atto/graphbuilder.h b/src/atto/graphbuilder.h index 1a36500..d65d854 100644 --- a/src/atto/graphbuilder.h +++ b/src/atto/graphbuilder.h @@ -6,8 +6,12 @@ #include #include #include +#include +#include using NodeId = std::string; +using BuilderError = std::string; +using BuilderResult = std::variant, BuilderError>; // A node under construction — holds structured parsed args instead of raw string. struct FlowNodeBuilder { @@ -36,8 +40,17 @@ struct GraphBuilder { // Deserializer: parses raw strings into FlowNodeBuilder, with error fallback. struct Deserializer { - // Parse a node from raw strings. On failure, returns an Error node builder. - static std::shared_ptr parse_node( + // Parse a node from pre-split args. Returns builder on success, error string on failure. + static BuilderResult parse_node( + const NodeId& id, const std::string& type, const std::vector& args); + + // Parse a node and add to graph. On failure, creates an Error node instead. + // Always returns a valid FlowNodeBuilder (added to gb). + static std::shared_ptr parse_or_error( const std::shared_ptr& gb, - const NodeId& id, const std::string& type, const std::string& args_str); + const NodeId& id, const std::string& type, const std::vector& args); + + // Parse an instrument@atto:0 stream into a GraphBuilder. + using ParseAttoResult = std::variant, BuilderError>; + static ParseAttoResult parse_atto(std::istream& f); }; From 12d455cc459e9298a73bf57e03c558383a286b0f Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Wed, 25 Mar 2026 23:46:43 +0200 Subject: [PATCH 08/86] Cleanups --- src/atto/args.cpp | 14 +++++++++++++- src/atto/args.h | 4 ++-- src/atto/graphbuilder.cpp | 3 +-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/atto/args.cpp b/src/atto/args.cpp index ad318e7..90a7c8d 100644 --- a/src/atto/args.cpp +++ b/src/atto/args.cpp @@ -342,7 +342,7 @@ SplitResult split_args(const std::string& args_str) { // ─── parse_args_v2: parse pre-split expressions ─── ParseResult parse_args_v2(const std::vector& exprs, bool is_expr) { - auto result = std::make_unique(); + auto result = std::make_shared(); auto register_slot = [&](int index, bool is_lambda) { result->slots[index] = is_lambda; @@ -366,6 +366,18 @@ ParseResult parse_args_v2(const std::vector& exprs, bool is_expr) { } } + // Validate: no gaps in slot indices + if (result->max_slot >= 0 && (int)result->slots.size() != result->max_slot + 1) { + std::string missing; + for (int i = 0; i <= result->max_slot; i++) { + if (result->slots.find(i) == result->slots.end()) { + if (!missing.empty()) missing += ", "; + missing += "$" + std::to_string(i); + } + } + return std::string("Missing pin reference(s): " + missing); + } + for (auto& expr : exprs) { result->args.push_back(parse_token(expr)); } diff --git a/src/atto/args.h b/src/atto/args.h index e5bb76b..04147bd 100644 --- a/src/atto/args.h +++ b/src/atto/args.h @@ -78,8 +78,8 @@ using SplitResult = std::variant, std::string>; SplitResult split_args(const std::string& args_str); // Parse pre-split expressions into ParsedArgs. -// Returns unique_ptr on success, or error string on failure. -using ParseResult = std::variant, std::string>; +// Returns shared_ptr on success, or error string on failure. +using ParseResult = std::variant, std::string>; ParseResult parse_args_v2(const std::vector& exprs, bool is_expr = false); // Reconstruct a space-separated args string from a ParsedArgs diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index 19f4f9c..5f1bb5a 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -115,8 +115,7 @@ BuilderResult Deserializer::parse_node( return BuilderError(*err); } - auto parsed = std::shared_ptr( - std::get>(std::move(parse_result)).release()); + auto parsed = std::get>(std::move(parse_result)); auto nb = std::make_shared(); nb->id = id; From 022c4a4eda69130038f989700e068c17f5de6076 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Wed, 25 Mar 2026 23:54:09 +0200 Subject: [PATCH 09/86] more graphbuilder work --- src/atto/args.cpp | 70 ++++++++++++++++++--------------------- src/atto/args.h | 21 +++++++++--- src/atto/graphbuilder.cpp | 8 ++--- src/atto/graphbuilder.h | 4 +-- 4 files changed, 54 insertions(+), 49 deletions(-) diff --git a/src/atto/args.cpp b/src/atto/args.cpp index 90a7c8d..bc25853 100644 --- a/src/atto/args.cpp +++ b/src/atto/args.cpp @@ -339,47 +339,44 @@ SplitResult split_args(const std::string& args_str) { return result; } -// ─── parse_args_v2: parse pre-split expressions ─── +// ─── parse_args_v2: parse pre-split expressions into ParsedArgs2 ─── -ParseResult parse_args_v2(const std::vector& exprs, bool is_expr) { - auto result = std::make_shared(); +static FlowArg2 parse_token_v2(const std::string& tok) { + if (tok.empty()) return ArgString{""}; - auto register_slot = [&](int index, bool is_lambda) { - result->slots[index] = is_lambda; - result->max_slot = std::max(result->max_slot, index); - }; + // Net reference: $name (non-numeric) + if (tok[0] == '$' && tok.size() >= 2 && !std::isdigit(tok[1])) { + return ArgNet{tok}; + } - // Scan all expressions for $N and @N refs - for (auto& expr : exprs) { - for (size_t i = 0; i < expr.size(); i++) { - if ((expr[i] == '$' || expr[i] == '@') && i + 1 < expr.size() && - expr[i + 1] >= '0' && expr[i + 1] <= '9') { - bool is_lambda = (expr[i] == '@'); - int n = 0; - size_t j = i + 1; - while (j < expr.size() && expr[j] >= '0' && expr[j] <= '9') { - n = n * 10 + (expr[j] - '0'); - j++; - } - register_slot(n, is_lambda); - } - } + // String literal + if (tok.front() == '"' && tok.back() == '"' && tok.size() >= 2) { + return ArgString{tok.substr(1, tok.size() - 2)}; } - // Validate: no gaps in slot indices - if (result->max_slot >= 0 && (int)result->slots.size() != result->max_slot + 1) { - std::string missing; - for (int i = 0; i <= result->max_slot; i++) { - if (result->slots.find(i) == result->slots.end()) { - if (!missing.empty()) missing += ", "; - missing += "$" + std::to_string(i); - } - } - return std::string("Missing pin reference(s): " + missing); + // Number + bool is_float = false; + bool is_number = true; + for (size_t i = 0; i < tok.size(); i++) { + char c = tok[i]; + if (c == '.' && !is_float) { is_float = true; continue; } + if (c == 'f' && i == tok.size() - 1) { is_float = true; continue; } + if (c == '-' && i == 0) continue; + if (c < '0' || c > '9') { is_number = false; break; } } + if (is_number && !tok.empty()) { + return ArgNumber{std::stod(tok), is_float}; + } + + // Expression (anything else — contains $N, @N, operators, function calls, etc.) + return ArgExpr{tok, -1, -1}; +} + +ParseResult parse_args_v2(const std::vector& exprs, bool is_expr) { + auto result = std::make_shared(); for (auto& expr : exprs) { - result->args.push_back(parse_token(expr)); + result->args.push_back(parse_token_v2(expr)); } result->has_any_args = !exprs.empty(); @@ -388,16 +385,13 @@ ParseResult parse_args_v2(const std::vector& exprs, bool is_expr) { // ─── reconstruct_args_str ─── -std::string reconstruct_args_str(const ParsedArgs& args) { +std::string reconstruct_args_str(const ParsedArgs2& args) { std::string result; for (auto& a : args.args) { if (!result.empty()) result += " "; std::visit([&](auto& v) { using T = std::decay_t; - if constexpr (std::is_same_v) result += "$" + std::to_string(v.index); - else if constexpr (std::is_same_v) result += "@" + std::to_string(v.index); - else if constexpr (std::is_same_v) result += v.name; - else if constexpr (std::is_same_v) result += "#" + v.enum_name + "." + v.value_name; + if constexpr (std::is_same_v) result += v.id; else if constexpr (std::is_same_v) { if (v.is_float) { char buf[64]; diff --git a/src/atto/args.h b/src/atto/args.h index 04147bd..d4eb06b 100644 --- a/src/atto/args.h +++ b/src/atto/args.h @@ -18,6 +18,17 @@ struct ArgExpr { std::string expr; int max_port; int max_lambda; }; // expressio using FlowArg = std::variant; +// v2 argument types +struct ArgNet { std::string id; }; // "$id" or "$unconnected" + +using FlowArg2 = std::variant; + +struct ParsedArgs2 { + std::vector args; + bool has_any_args = false; +}; + +// Legacy struct ParsedArgs { std::vector args; @@ -77,10 +88,10 @@ ParsedArgs parse_args(const std::string& args_str, bool is_expr = false); using SplitResult = std::variant, std::string>; SplitResult split_args(const std::string& args_str); -// Parse pre-split expressions into ParsedArgs. -// Returns shared_ptr on success, or error string on failure. -using ParseResult = std::variant, std::string>; +// Parse pre-split expressions into ParsedArgs2. +// Returns shared_ptr on success, or error string on failure. +using ParseResult = std::variant, std::string>; ParseResult parse_args_v2(const std::vector& exprs, bool is_expr = false); -// Reconstruct a space-separated args string from a ParsedArgs -std::string reconstruct_args_str(const ParsedArgs& args); +// Reconstruct a space-separated args string from ParsedArgs2 +std::string reconstruct_args_str(const ParsedArgs2& args); diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index 5f1bb5a..b63fd23 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -65,7 +65,7 @@ std::string FlowNodeBuilder::args_str() const { // ─── GraphBuilder ─── -std::shared_ptr GraphBuilder::add(NodeId id, NodeTypeID type, std::shared_ptr args) { +std::shared_ptr GraphBuilder::add(NodeId id, NodeTypeID type, std::shared_ptr args) { auto nb = std::make_shared(); nb->id = std::move(id); nb->type_id = type; @@ -101,7 +101,7 @@ BuilderResult Deserializer::parse_node( auto nb = std::make_shared(); nb->id = id; nb->type_id = type_id; - nb->parsed_args = std::make_shared(); + nb->parsed_args = std::make_shared(); nb->parsed_args->args.push_back(ArgString{args[0]}); nb->parsed_args->has_any_args = true; return nb; @@ -115,7 +115,7 @@ BuilderResult Deserializer::parse_node( return BuilderError(*err); } - auto parsed = std::get>(std::move(parse_result)); + auto parsed = std::get>(std::move(parse_result)); auto nb = std::make_shared(); nb->id = id; @@ -145,7 +145,7 @@ std::shared_ptr Deserializer::parse_or_error( auto nb = std::make_shared(); nb->id = id; nb->type_id = NodeTypeID::Error; - nb->parsed_args = std::make_shared(); + nb->parsed_args = std::make_shared(); nb->parsed_args->args.push_back(ArgString{type + " " + args_joined}); nb->parsed_args->has_any_args = true; nb->error = error_msg; diff --git a/src/atto/graphbuilder.h b/src/atto/graphbuilder.h index d65d854..325c540 100644 --- a/src/atto/graphbuilder.h +++ b/src/atto/graphbuilder.h @@ -17,7 +17,7 @@ using BuilderResult = std::variant, Buil struct FlowNodeBuilder { NodeId id; NodeTypeID type_id = NodeTypeID::Unknown; - std::shared_ptr parsed_args; + std::shared_ptr parsed_args; Vec2 position = {0, 0}; bool shadow = false; std::string error; @@ -31,7 +31,7 @@ struct GraphBuilder { std::vector> builders; // Add a pre-built node - std::shared_ptr add(NodeId id, NodeTypeID type, std::shared_ptr args); + std::shared_ptr add(NodeId id, NodeTypeID type, std::shared_ptr args); void link(const std::string& from, const std::string& to); From d0c9eb15cb4614707bf6d2c939b5a32d42d827b9 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 00:18:26 +0200 Subject: [PATCH 10/86] work on nets and nodes --- src/atto/graphbuilder.cpp | 172 ++++++++++++++++++++++++++------------ src/atto/graphbuilder.h | 58 ++++++++++--- 2 files changed, 165 insertions(+), 65 deletions(-) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index b63fd23..6d8a89c 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -4,7 +4,7 @@ #include "node_types.h" #include -// ─── TOML helpers (shared with serial.cpp) ─── +// ─── TOML helpers ─── static std::string trim(std::string s) { while (!s.empty() && (s.front() == ' ' || s.front() == '\t')) s.erase(s.begin()); @@ -56,6 +56,35 @@ static std::vector parse_toml_array(const std::string& val) { return result; } +// ─── Validation helper ─── + +static void validate_weak_is_node(const BuilderEntryWeak& w) { + auto p = w.lock(); + if (!p) return; // expired is ok + if (!std::holds_alternative(*p)) + throw std::logic_error("NetBuilder: weak ref points to a NetBuilder, not a FlowNodeBuilder"); +} + +// ─── NetBuilder ─── + +void NetBuilder::compact() { + validate(); + destinations.erase( + std::remove_if(destinations.begin(), destinations.end(), [](auto& w) { return w.expired(); }), + destinations.end()); +} + +bool NetBuilder::unused() { + compact(); + return source.expired() && destinations.empty(); +} + +void NetBuilder::validate() const { + validate_weak_is_node(source); + for (auto& d : destinations) + validate_weak_is_node(d); +} + // ─── FlowNodeBuilder ─── std::string FlowNodeBuilder::args_str() const { @@ -65,22 +94,59 @@ std::string FlowNodeBuilder::args_str() const { // ─── GraphBuilder ─── -std::shared_ptr GraphBuilder::add(NodeId id, NodeTypeID type, std::shared_ptr args) { - auto nb = std::make_shared(); - nb->id = std::move(id); - nb->type_id = type; - nb->parsed_args = std::move(args); - builders.push_back(nb); +FlowNodeBuilder& GraphBuilder::add_node(NodeId id, NodeTypeID type, std::shared_ptr args) { + auto entry = std::make_shared(FlowNodeBuilder{}); + auto& nb = std::get(*entry); + nb.type_id = type; + nb.parsed_args = std::move(args); + entries[std::move(id)] = entry; return nb; } -void GraphBuilder::link(const std::string& from, const std::string& to) { - // TODO: links between FlowNodeBuilders +std::pair GraphBuilder::find_or_create_net(const NodeId& name, bool for_source) { + auto [id, ptr] = find_net(name); // throws if name exists as a node + if (ptr) { + if (for_source && !std::get(*ptr).source.expired()) + throw std::logic_error("find_or_create_net(\"" + name + "\"): net already has a source"); + return {id, ptr}; + } + auto entry = std::make_shared(NetBuilder{}); + auto& net = std::get(*entry); + net.auto_wire = (name.size() >= 6 && name.substr(0, 6) == "$auto-"); + entries[name] = entry; + return {entries.find(name)->first, entry}; +} + +BuilderEntryPtr GraphBuilder::find(const NodeId& id) { + auto it = entries.find(id); + return (it != entries.end()) ? it->second : nullptr; +} + +std::pair GraphBuilder::find_node(const NodeId& id) { + auto it = entries.find(id); + if (it == entries.end()) return {id, nullptr}; + if (!std::holds_alternative(*it->second)) + throw std::logic_error("find_node(\"" + id + "\"): entry exists but is a NetBuilder, not a FlowNodeBuilder"); + return {it->first, it->second}; } -std::shared_ptr GraphBuilder::find(const NodeId& id) { - for (auto& nb : builders) if (nb->id == id) return nb; - return nullptr; +std::pair GraphBuilder::find_net(const NodeId& name) { + auto it = entries.find(name); + if (it == entries.end()) return {name, nullptr}; + if (!std::holds_alternative(*it->second)) + throw std::logic_error("find_net(\"" + name + "\"): entry exists but is a FlowNodeBuilder, not a NetBuilder"); + return {it->first, it->second}; +} + +void GraphBuilder::compact() { + for (auto it = entries.begin(); it != entries.end(); ) { + if (std::holds_alternative(*it->second) && + std::get(*it->second).unused()) { + it = entries.erase(it); + } else { + ++it; + } + } } // ─── Deserializer ─── @@ -94,69 +160,61 @@ BuilderResult Deserializer::parse_node( return BuilderError("Unknown node type: " + type); } - // Labels and errors: exactly 1 ArgString + FlowNodeBuilder nb; + nb.type_id = type_id; + if (is_any_of(type_id, NodeTypeID::Label, NodeTypeID::Error)) { if (args.size() != 1) throw std::invalid_argument("Label/Error node requires exactly 1 argument, got " + std::to_string(args.size())); - auto nb = std::make_shared(); - nb->id = id; - nb->type_id = type_id; - nb->parsed_args = std::make_shared(); - nb->parsed_args->args.push_back(ArgString{args[0]}); - nb->parsed_args->has_any_args = true; - return nb; + nb.parsed_args = std::make_shared(); + nb.parsed_args->args.push_back(ArgString{args[0]}); + nb.parsed_args->has_any_args = true; + return std::pair{id, std::move(nb)}; } bool is_expr = is_any_of(type_id, NodeTypeID::Expr, NodeTypeID::ExprBang); - // Parse expressions (args are already split) auto parse_result = parse_args_v2(args, is_expr); if (auto* err = std::get_if(&parse_result)) { return BuilderError(*err); } - auto parsed = std::get>(std::move(parse_result)); - - auto nb = std::make_shared(); - nb->id = id; - nb->type_id = type_id; - nb->parsed_args = std::move(parsed); - return nb; + nb.parsed_args = std::get>(std::move(parse_result)); + return std::pair{id, std::move(nb)}; } -std::shared_ptr Deserializer::parse_or_error( - const std::shared_ptr& gb, +FlowNodeBuilder& Deserializer::parse_or_error( + GraphBuilder& gb, const NodeId& id, const std::string& type, const std::vector& args) { auto result = parse_node(id, type, args); - if (auto* nb = std::get_if>(&result)) { - gb->builders.push_back(*nb); - return *nb; + if (auto* p = std::get_if>(&result)) { + auto entry = std::make_shared(std::move(p->second)); + gb.entries[p->first] = entry; + return std::get(*entry); } auto& error_msg = std::get(result); - // Reconstruct original args for error display std::string args_joined; for (auto& a : args) { if (!args_joined.empty()) args_joined += " "; args_joined += a; } - auto nb = std::make_shared(); - nb->id = id; - nb->type_id = NodeTypeID::Error; - nb->parsed_args = std::make_shared(); - nb->parsed_args->args.push_back(ArgString{type + " " + args_joined}); - nb->parsed_args->has_any_args = true; - nb->error = error_msg; - gb->builders.push_back(nb); - return nb; + FlowNodeBuilder nb; + nb.type_id = NodeTypeID::Error; + nb.parsed_args = std::make_shared(); + nb.parsed_args->args.push_back(ArgString{type + " " + args_joined}); + nb.parsed_args->has_any_args = true; + nb.error = error_msg; + auto entry = std::make_shared(std::move(nb)); + gb.entries[id] = entry; + return std::get(*entry); } -// ─── parse_atto: instrument@atto:0 stream parser ─── +// ─── parse_atto ─── Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { - // Check version header std::string first_line; while (std::getline(f, first_line)) { first_line = trim(first_line); @@ -168,7 +226,6 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { auto gb = std::make_shared(); - // Parse state bool in_node = false; std::string cur_id, cur_type; std::vector cur_args; @@ -186,12 +243,24 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { cur_id = "$auto-" + generate_guid(); } - auto nb = parse_or_error(gb, cur_id, cur_type, cur_args); - nb->position = {cur_x, cur_y}; - nb->shadow = cur_shadow; + auto& nb = parse_or_error(*gb, cur_id, cur_type, cur_args); + nb.position = {cur_x, cur_y}; + nb.shadow = cur_shadow; - // Store inputs/outputs net refs on the builder for later resolution - // TODO: resolve net connections after all nodes are parsed + auto node_entry = gb->find(cur_id); + + // Wire nets from outputs (this node is source) + for (auto& net_name : cur_outputs) { + if (net_name.empty()) continue; + auto [_, net_ptr] = gb->find_or_create_net(net_name, true); + std::get(*net_ptr).source = node_entry; + } + // Wire nets from inputs (this node is destination) + for (auto& net_name : cur_inputs) { + if (net_name.empty()) continue; + auto [_, net_ptr] = gb->find_or_create_net(net_name); + std::get(*net_ptr).destinations.push_back(node_entry); + } cur_id.clear(); cur_type.clear(); cur_args.clear(); cur_inputs.clear(); cur_outputs.clear(); @@ -210,7 +279,6 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { } if (line.find("# version") == 0) continue; - if (!in_node) continue; auto eq = line.find('='); diff --git a/src/atto/graphbuilder.h b/src/atto/graphbuilder.h index 325c540..1ce2b1b 100644 --- a/src/atto/graphbuilder.h +++ b/src/atto/graphbuilder.h @@ -5,17 +5,42 @@ #include "node_types.h" #include #include +#include #include #include #include +#include +#include using NodeId = std::string; using BuilderError = std::string; -using BuilderResult = std::variant, BuilderError>; + +struct FlowNodeBuilder; +struct NetBuilder; + +using BuilderEntry = std::variant; +using BuilderEntryPtr = std::shared_ptr; +using BuilderEntryWeak = std::weak_ptr; + +// Named wire — one source, many destinations (weak refs to BuilderEntry, must be FlowNodeBuilder). +struct NetBuilder { + bool auto_wire = false; + + BuilderEntryWeak source; + std::vector destinations; + + // Remove expired weak refs, throw if any live ref is not a FlowNodeBuilder + void compact(); + + // compact + check if net has no live source and no live destinations + bool unused(); + + // Throw if any live weak ref is not a FlowNodeBuilder (or null) + void validate() const; +}; // A node under construction — holds structured parsed args instead of raw string. struct FlowNodeBuilder { - NodeId id; NodeTypeID type_id = NodeTypeID::Unknown; std::shared_ptr parsed_args; Vec2 position = {0, 0}; @@ -26,31 +51,38 @@ struct FlowNodeBuilder { std::string args_str() const; }; +using BuilderResult = std::variant, BuilderError>; + struct GraphBuilder { TypePool pool; - std::vector> builders; + std::map entries; + + // Add a node + FlowNodeBuilder& add_node(NodeId id, NodeTypeID type, std::shared_ptr args); + + // Get or create a net — throws if name exists as a node, or if for_source and source already set + std::pair find_or_create_net(const NodeId& name, bool for_source = false); - // Add a pre-built node - std::shared_ptr add(NodeId id, NodeTypeID type, std::shared_ptr args); + // Find any entry by id + BuilderEntryPtr find(const NodeId& id); - void link(const std::string& from, const std::string& to); + // Find typed — throws if id exists but is wrong type + std::pair find_node(const NodeId& id); + std::pair find_net(const NodeId& name); - std::shared_ptr find(const NodeId& id); + // Remove unused nets + void compact(); }; // Deserializer: parses raw strings into FlowNodeBuilder, with error fallback. struct Deserializer { - // Parse a node from pre-split args. Returns builder on success, error string on failure. static BuilderResult parse_node( const NodeId& id, const std::string& type, const std::vector& args); - // Parse a node and add to graph. On failure, creates an Error node instead. - // Always returns a valid FlowNodeBuilder (added to gb). - static std::shared_ptr parse_or_error( - const std::shared_ptr& gb, + static FlowNodeBuilder& parse_or_error( + GraphBuilder& gb, const NodeId& id, const std::string& type, const std::vector& args); - // Parse an instrument@atto:0 stream into a GraphBuilder. using ParseAttoResult = std::variant, BuilderError>; static ParseAttoResult parse_atto(std::istream& f); }; From 2e7b6c54bbe685e399c29de853d78f534f33faf9 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 00:23:45 +0200 Subject: [PATCH 11/86] args2 to graphbuilder --- src/atto/args.cpp | 69 +----------------------------------- src/atto/args.h | 19 ---------- src/atto/graphbuilder.cpp | 74 +++++++++++++++++++++++++++++++++++---- src/atto/graphbuilder.h | 32 ++++++++++------- 4 files changed, 88 insertions(+), 106 deletions(-) diff --git a/src/atto/args.cpp b/src/atto/args.cpp index bc25853..147cd21 100644 --- a/src/atto/args.cpp +++ b/src/atto/args.cpp @@ -339,71 +339,4 @@ SplitResult split_args(const std::string& args_str) { return result; } -// ─── parse_args_v2: parse pre-split expressions into ParsedArgs2 ─── - -static FlowArg2 parse_token_v2(const std::string& tok) { - if (tok.empty()) return ArgString{""}; - - // Net reference: $name (non-numeric) - if (tok[0] == '$' && tok.size() >= 2 && !std::isdigit(tok[1])) { - return ArgNet{tok}; - } - - // String literal - if (tok.front() == '"' && tok.back() == '"' && tok.size() >= 2) { - return ArgString{tok.substr(1, tok.size() - 2)}; - } - - // Number - bool is_float = false; - bool is_number = true; - for (size_t i = 0; i < tok.size(); i++) { - char c = tok[i]; - if (c == '.' && !is_float) { is_float = true; continue; } - if (c == 'f' && i == tok.size() - 1) { is_float = true; continue; } - if (c == '-' && i == 0) continue; - if (c < '0' || c > '9') { is_number = false; break; } - } - if (is_number && !tok.empty()) { - return ArgNumber{std::stod(tok), is_float}; - } - - // Expression (anything else — contains $N, @N, operators, function calls, etc.) - return ArgExpr{tok, -1, -1}; -} - -ParseResult parse_args_v2(const std::vector& exprs, bool is_expr) { - auto result = std::make_shared(); - - for (auto& expr : exprs) { - result->args.push_back(parse_token_v2(expr)); - } - result->has_any_args = !exprs.empty(); - - return result; -} - -// ─── reconstruct_args_str ─── - -std::string reconstruct_args_str(const ParsedArgs2& args) { - std::string result; - for (auto& a : args.args) { - if (!result.empty()) result += " "; - std::visit([&](auto& v) { - using T = std::decay_t; - if constexpr (std::is_same_v) result += v.id; - else if constexpr (std::is_same_v) { - if (v.is_float) { - char buf[64]; - snprintf(buf, sizeof(buf), "%g", v.value); - result += buf; - } else { - result += std::to_string((long long)v.value); - } - } - else if constexpr (std::is_same_v) result += "\"" + v.value + "\""; - else if constexpr (std::is_same_v) result += v.expr; - }, a); - } - return result; -} +// (v2 types and functions moved to graphbuilder.h/cpp) diff --git a/src/atto/args.h b/src/atto/args.h index d4eb06b..01f5c61 100644 --- a/src/atto/args.h +++ b/src/atto/args.h @@ -18,17 +18,6 @@ struct ArgExpr { std::string expr; int max_port; int max_lambda; }; // expressio using FlowArg = std::variant; -// v2 argument types -struct ArgNet { std::string id; }; // "$id" or "$unconnected" - -using FlowArg2 = std::variant; - -struct ParsedArgs2 { - std::vector args; - bool has_any_args = false; -}; - -// Legacy struct ParsedArgs { std::vector args; @@ -87,11 +76,3 @@ ParsedArgs parse_args(const std::string& args_str, bool is_expr = false); // Returns vector on success, or error string on failure (mismatched parens/braces/quotes). using SplitResult = std::variant, std::string>; SplitResult split_args(const std::string& args_str); - -// Parse pre-split expressions into ParsedArgs2. -// Returns shared_ptr on success, or error string on failure. -using ParseResult = std::variant, std::string>; -ParseResult parse_args_v2(const std::vector& exprs, bool is_expr = false); - -// Reconstruct a space-separated args string from ParsedArgs2 -std::string reconstruct_args_str(const ParsedArgs2& args); diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index 6d8a89c..ddfdabd 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -1,8 +1,6 @@ #include "graphbuilder.h" -#include "args.h" -#include "expr.h" -#include "node_types.h" #include +#include // ─── TOML helpers ─── @@ -85,6 +83,70 @@ void NetBuilder::validate() const { validate_weak_is_node(d); } +// ─── v2 parse/reconstruct ─── + +static FlowArg2 parse_token_v2(const std::string& tok) { + if (tok.empty()) return ArgString2{""}; + + // Net reference: $name (non-numeric) + if (tok[0] == '$' && tok.size() >= 2 && !std::isdigit(tok[1])) { + return ArgNet2{tok}; + } + + // String literal + if (tok.front() == '"' && tok.back() == '"' && tok.size() >= 2) { + return ArgString2{tok.substr(1, tok.size() - 2)}; + } + + // Number + bool is_float = false; + bool is_number = true; + for (size_t i = 0; i < tok.size(); i++) { + char c = tok[i]; + if (c == '.' && !is_float) { is_float = true; continue; } + if (c == 'f' && i == tok.size() - 1) { is_float = true; continue; } + if (c == '-' && i == 0) continue; + if (c < '0' || c > '9') { is_number = false; break; } + } + if (is_number && !tok.empty()) { + return ArgNumber2{std::stod(tok), is_float}; + } + + // Expression (anything else) + return ArgExpr2{tok}; +} + +ParseResult parse_args_v2(const std::vector& exprs, bool is_expr) { + auto result = std::make_shared(); + for (auto& expr : exprs) { + result->push_back(parse_token_v2(expr)); + } + return result; +} + +std::string reconstruct_args_str(const ParsedArgs2& args) { + std::string result; + for (auto& a : args) { + if (!result.empty()) result += " "; + std::visit([&](auto& v) { + using T = std::decay_t; + if constexpr (std::is_same_v) result += v.id; + else if constexpr (std::is_same_v) { + if (v.is_float) { + char buf[64]; + snprintf(buf, sizeof(buf), "%g", v.value); + result += buf; + } else { + result += std::to_string((long long)v.value); + } + } + else if constexpr (std::is_same_v) result += "\"" + v.value + "\""; + else if constexpr (std::is_same_v) result += v.expr; + }, a); + } + return result; +} + // ─── FlowNodeBuilder ─── std::string FlowNodeBuilder::args_str() const { @@ -167,8 +229,7 @@ BuilderResult Deserializer::parse_node( if (args.size() != 1) throw std::invalid_argument("Label/Error node requires exactly 1 argument, got " + std::to_string(args.size())); nb.parsed_args = std::make_shared(); - nb.parsed_args->args.push_back(ArgString{args[0]}); - nb.parsed_args->has_any_args = true; + nb.parsed_args->push_back(ArgString2{args[0]}); return std::pair{id, std::move(nb)}; } @@ -204,8 +265,7 @@ FlowNodeBuilder& Deserializer::parse_or_error( FlowNodeBuilder nb; nb.type_id = NodeTypeID::Error; nb.parsed_args = std::make_shared(); - nb.parsed_args->args.push_back(ArgString{type + " " + args_joined}); - nb.parsed_args->has_any_args = true; + nb.parsed_args->push_back(ArgString2{type + " " + args_joined}); nb.error = error_msg; auto entry = std::make_shared(std::move(nb)); gb.entries[id] = entry; diff --git a/src/atto/graphbuilder.h b/src/atto/graphbuilder.h index 1ce2b1b..3517f3c 100644 --- a/src/atto/graphbuilder.h +++ b/src/atto/graphbuilder.h @@ -1,7 +1,6 @@ #pragma once #include "model.h" #include "types.h" -#include "args.h" #include "node_types.h" #include #include @@ -11,10 +10,30 @@ #include #include #include +#include using NodeId = std::string; using BuilderError = std::string; +// ─── v2 argument types (independent of legacy args.h) ─── + +struct ArgNet2 { std::string id; }; // "$id" or "$unconnected" +struct ArgNumber2 { double value; bool is_float; }; // 42, 3.14 +struct ArgString2 { std::string value; }; // "hello\"world" +struct ArgExpr2 { std::string expr; }; // expression (contains $N, @N, operators, etc.) + +using FlowArg2 = std::variant; +using ParsedArgs2 = std::vector; + +// Parse pre-split expressions into ParsedArgs2. +using ParseResult = std::variant, std::string>; +ParseResult parse_args_v2(const std::vector& exprs, bool is_expr = false); + +// Reconstruct a space-separated args string from ParsedArgs2 +std::string reconstruct_args_str(const ParsedArgs2& args); + +// ─── Builder types ─── + struct FlowNodeBuilder; struct NetBuilder; @@ -29,13 +48,8 @@ struct NetBuilder { BuilderEntryWeak source; std::vector destinations; - // Remove expired weak refs, throw if any live ref is not a FlowNodeBuilder void compact(); - - // compact + check if net has no live source and no live destinations bool unused(); - - // Throw if any live weak ref is not a FlowNodeBuilder (or null) void validate() const; }; @@ -47,7 +61,6 @@ struct FlowNodeBuilder { bool shadow = false; std::string error; - // Reconstruct args string (for legacy code) std::string args_str() const; }; @@ -57,20 +70,15 @@ struct GraphBuilder { TypePool pool; std::map entries; - // Add a node FlowNodeBuilder& add_node(NodeId id, NodeTypeID type, std::shared_ptr args); - // Get or create a net — throws if name exists as a node, or if for_source and source already set std::pair find_or_create_net(const NodeId& name, bool for_source = false); - // Find any entry by id BuilderEntryPtr find(const NodeId& id); - // Find typed — throws if id exists but is wrong type std::pair find_node(const NodeId& id); std::pair find_net(const NodeId& name); - // Remove unused nets void compact(); }; From 28bcd4c6d6bfbf3ffa90d3078f4a1952e738c98d Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 00:29:34 +0200 Subject: [PATCH 12/86] more net work --- src/atto/graphbuilder.cpp | 31 +++++++++++++++++-------------- src/atto/graphbuilder.h | 36 +++++++++++++++++++++--------------- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index ddfdabd..8a278b8 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -85,12 +85,13 @@ void NetBuilder::validate() const { // ─── v2 parse/reconstruct ─── -static FlowArg2 parse_token_v2(const std::string& tok) { +static FlowArg2 parse_token_v2(GraphBuilder& gb, const std::string& tok) { if (tok.empty()) return ArgString2{""}; // Net reference: $name (non-numeric) if (tok[0] == '$' && tok.size() >= 2 && !std::isdigit(tok[1])) { - return ArgNet2{tok}; + auto [id, entry] = gb.find_or_create_net(tok, false); + return ArgNet2{NodeId(id), entry}; } // String literal @@ -116,10 +117,11 @@ static FlowArg2 parse_token_v2(const std::string& tok) { return ArgExpr2{tok}; } -ParseResult parse_args_v2(const std::vector& exprs, bool is_expr) { +ParseResult parse_args_v2(const std::shared_ptr& gb, + const std::vector& exprs, bool is_expr) { auto result = std::make_shared(); for (auto& expr : exprs) { - result->push_back(parse_token_v2(expr)); + result->push_back(parse_token_v2(*gb, expr)); } return result; } @@ -130,7 +132,7 @@ std::string reconstruct_args_str(const ParsedArgs2& args) { if (!result.empty()) result += " "; std::visit([&](auto& v) { using T = std::decay_t; - if constexpr (std::is_same_v) result += v.id; + if constexpr (std::is_same_v) result += v.first; else if constexpr (std::is_same_v) { if (v.is_float) { char buf[64]; @@ -165,7 +167,7 @@ FlowNodeBuilder& GraphBuilder::add_node(NodeId id, NodeTypeID type, std::shared_ return nb; } -std::pair GraphBuilder::find_or_create_net(const NodeId& name, bool for_source) { +std::pair GraphBuilder::find_or_create_net(const NodeId& name, bool for_source) { auto [id, ptr] = find_net(name); // throws if name exists as a node if (ptr) { if (for_source && !std::get(*ptr).source.expired()) @@ -184,7 +186,7 @@ BuilderEntryPtr GraphBuilder::find(const NodeId& id) { return (it != entries.end()) ? it->second : nullptr; } -std::pair GraphBuilder::find_node(const NodeId& id) { +std::pair GraphBuilder::find_node(const NodeId& id) { auto it = entries.find(id); if (it == entries.end()) return {id, nullptr}; if (!std::holds_alternative(*it->second)) @@ -192,7 +194,7 @@ std::pair GraphBuilder::find_node(const NodeId& return {it->first, it->second}; } -std::pair GraphBuilder::find_net(const NodeId& name) { +std::pair GraphBuilder::find_net(const NodeId& name) { auto it = entries.find(name); if (it == entries.end()) return {name, nullptr}; if (!std::holds_alternative(*it->second)) @@ -214,6 +216,7 @@ void GraphBuilder::compact() { // ─── Deserializer ─── BuilderResult Deserializer::parse_node( + const std::shared_ptr& gb, const NodeId& id, const std::string& type, const std::vector& args) { NodeTypeID type_id = node_type_id_from_string(type.c_str()); @@ -235,7 +238,7 @@ BuilderResult Deserializer::parse_node( bool is_expr = is_any_of(type_id, NodeTypeID::Expr, NodeTypeID::ExprBang); - auto parse_result = parse_args_v2(args, is_expr); + auto parse_result = parse_args_v2(gb, args, is_expr); if (auto* err = std::get_if(&parse_result)) { return BuilderError(*err); } @@ -245,14 +248,14 @@ BuilderResult Deserializer::parse_node( } FlowNodeBuilder& Deserializer::parse_or_error( - GraphBuilder& gb, + const std::shared_ptr& gb, const NodeId& id, const std::string& type, const std::vector& args) { - auto result = parse_node(id, type, args); + auto result = parse_node(gb, id, type, args); if (auto* p = std::get_if>(&result)) { auto entry = std::make_shared(std::move(p->second)); - gb.entries[p->first] = entry; + gb->entries[p->first] = entry; return std::get(*entry); } @@ -268,7 +271,7 @@ FlowNodeBuilder& Deserializer::parse_or_error( nb.parsed_args->push_back(ArgString2{type + " " + args_joined}); nb.error = error_msg; auto entry = std::make_shared(std::move(nb)); - gb.entries[id] = entry; + gb->entries[id] = entry; return std::get(*entry); } @@ -303,7 +306,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { cur_id = "$auto-" + generate_guid(); } - auto& nb = parse_or_error(*gb, cur_id, cur_type, cur_args); + auto& nb = parse_or_error(gb, cur_id, cur_type, cur_args); nb.position = {cur_x, cur_y}; nb.shadow = cur_shadow; diff --git a/src/atto/graphbuilder.h b/src/atto/graphbuilder.h index 3517f3c..43fd2c5 100644 --- a/src/atto/graphbuilder.h +++ b/src/atto/graphbuilder.h @@ -15,9 +15,18 @@ using NodeId = std::string; using BuilderError = std::string; -// ─── v2 argument types (independent of legacy args.h) ─── +// ─── Forward declarations & aliases ─── -struct ArgNet2 { std::string id; }; // "$id" or "$unconnected" +struct FlowNodeBuilder; +struct NetBuilder; + +using BuilderEntry = std::variant; +using BuilderEntryPtr = std::shared_ptr; +using BuilderEntryWeak = std::weak_ptr; + +// ─── v2 argument types ─── + +using ArgNet2 = std::pair; // resolved net ref from find_or_create_net struct ArgNumber2 { double value; bool is_float; }; // 42, 3.14 struct ArgString2 { std::string value; }; // "hello\"world" struct ArgExpr2 { std::string expr; }; // expression (contains $N, @N, operators, etc.) @@ -25,22 +34,18 @@ struct ArgExpr2 { std::string expr; }; // expression (contains using FlowArg2 = std::variant; using ParsedArgs2 = std::vector; -// Parse pre-split expressions into ParsedArgs2. +struct GraphBuilder; // forward for parse_args_v2 + +// Parse pre-split expressions into ParsedArgs2. Resolves $name tokens via gb. using ParseResult = std::variant, std::string>; -ParseResult parse_args_v2(const std::vector& exprs, bool is_expr = false); +ParseResult parse_args_v2(const std::shared_ptr& gb, + const std::vector& exprs, bool is_expr = false); // Reconstruct a space-separated args string from ParsedArgs2 std::string reconstruct_args_str(const ParsedArgs2& args); // ─── Builder types ─── -struct FlowNodeBuilder; -struct NetBuilder; - -using BuilderEntry = std::variant; -using BuilderEntryPtr = std::shared_ptr; -using BuilderEntryWeak = std::weak_ptr; - // Named wire — one source, many destinations (weak refs to BuilderEntry, must be FlowNodeBuilder). struct NetBuilder { bool auto_wire = false; @@ -72,12 +77,12 @@ struct GraphBuilder { FlowNodeBuilder& add_node(NodeId id, NodeTypeID type, std::shared_ptr args); - std::pair find_or_create_net(const NodeId& name, bool for_source = false); + std::pair find_or_create_net(const NodeId& name, bool for_source = false); BuilderEntryPtr find(const NodeId& id); - std::pair find_node(const NodeId& id); - std::pair find_net(const NodeId& name); + std::pair find_node(const NodeId& id); + std::pair find_net(const NodeId& name); void compact(); }; @@ -85,10 +90,11 @@ struct GraphBuilder { // Deserializer: parses raw strings into FlowNodeBuilder, with error fallback. struct Deserializer { static BuilderResult parse_node( + const std::shared_ptr& gb, const NodeId& id, const std::string& type, const std::vector& args); static FlowNodeBuilder& parse_or_error( - GraphBuilder& gb, + const std::shared_ptr& gb, const NodeId& id, const std::string& type, const std::vector& args); using ParseAttoResult = std::variant, BuilderError>; From a7a7e00c4bb8e249a0ef7e4ee9ee5fc74bd5ea20 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 01:17:09 +0200 Subject: [PATCH 13/86] Editor2Pane! --- CMakeLists.txt | 1 + src/atto/graphbuilder.cpp | 34 ++++ src/atto/graphbuilder.h | 6 +- src/atto/node_types2.h | 381 ++++++++++++++++++++++++++++++++++++++ src/attoflow/editor.cpp | 21 ++- src/attoflow/editor.h | 5 +- src/attoflow/editor2.cpp | 233 +++++++++++++++++++++++ src/attoflow/editor2.h | 43 +++++ 8 files changed, 719 insertions(+), 5 deletions(-) create mode 100644 src/atto/node_types2.h create mode 100644 src/attoflow/editor2.cpp create mode 100644 src/attoflow/editor2.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 749ec0e..2d76349 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,7 @@ if(ATTOLANG_BUILD_EDITOR) add_executable(attoflow src/attoflow/main.cpp src/attoflow/editor.cpp + src/attoflow/editor2.cpp ) target_include_directories(attoflow PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index 8a278b8..cca4ac6 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -1,6 +1,7 @@ #include "graphbuilder.h" #include #include +#include // ─── TOML helpers ─── @@ -120,6 +121,39 @@ static FlowArg2 parse_token_v2(GraphBuilder& gb, const std::string& tok) { ParseResult parse_args_v2(const std::shared_ptr& gb, const std::vector& exprs, bool is_expr) { auto result = std::make_shared(); + + // Scan all expressions for $N refs to compute rewrite_input_count + std::set slot_indices; + for (auto& expr : exprs) { + for (size_t i = 0; i < expr.size(); i++) { + if (expr[i] == '$' && i + 1 < expr.size() && std::isdigit(expr[i + 1])) { + int n = 0; + size_t j = i + 1; + while (j < expr.size() && std::isdigit(expr[j])) { + n = n * 10 + (expr[j] - '0'); + j++; + } + slot_indices.insert(n); + } + } + } + + // Validate contiguous from 0 + if (!slot_indices.empty()) { + int max_slot = *slot_indices.rbegin(); + if ((int)slot_indices.size() != max_slot + 1) { + std::string missing; + for (int i = 0; i <= max_slot; i++) { + if (!slot_indices.count(i)) { + if (!missing.empty()) missing += ", "; + missing += "$" + std::to_string(i); + } + } + return std::string("Missing pin reference(s): " + missing); + } + result->rewrite_input_count = max_slot + 1; + } + for (auto& expr : exprs) { result->push_back(parse_token_v2(*gb, expr)); } diff --git a/src/atto/graphbuilder.h b/src/atto/graphbuilder.h index 43fd2c5..2cce793 100644 --- a/src/atto/graphbuilder.h +++ b/src/atto/graphbuilder.h @@ -32,7 +32,11 @@ struct ArgString2 { std::string value; }; // "hello\"world" struct ArgExpr2 { std::string expr; }; // expression (contains $N, @N, operators, etc.) using FlowArg2 = std::variant; -using ParsedArgs2 = std::vector; + +struct ParsedArgs2 : std::vector { + using vector::vector; + int rewrite_input_count = 0; // count of unique $N refs across all expressions (contiguous from $0) +}; struct GraphBuilder; // forward for parse_args_v2 diff --git a/src/atto/node_types2.h b/src/atto/node_types2.h new file mode 100644 index 0000000..7305423 --- /dev/null +++ b/src/atto/node_types2.h @@ -0,0 +1,381 @@ +#pragma once +#include "node_types.h" // for NodeTypeID + +// New pin model: flattened inputs/outputs, optional, va_args + +enum class PortKind2 : uint8_t { + BangTrigger, // bang input (rendered as square, top) + Data, // data input/output + Lambda, // lambda capture (only accepts node refs) + BangNext, // bang output (rendered as square) +}; + +struct PortDesc2 { + const char* name; + const char* desc; + PortKind2 kind = PortKind2::Data; + const char* type_name = nullptr; + bool optional = false; + bool va_args = false; // last pin only: repeats as name_0, name_1, ... +}; + +struct NodeType2 { + NodeTypeID type_id; + const char* name; + const char* desc; + const PortDesc2* input_ports; + int num_inputs; + const PortDesc2* output_ports; + int num_outputs; + bool is_event = false; + bool is_declaration = false; +}; + +// ─── Port descriptor arrays ─── + +// Common outputs +static const PortDesc2 P2_NEXT[] = {{"next", "fires after completion", PortKind2::BangNext}}; +static const PortDesc2 P2_RESULT[] = {{"result", "result value"}}; +static const PortDesc2 P2_NEXT_RESULT[] = {{"next", "fires after completion", PortKind2::BangNext}, {"result", "result value"}}; + +// Common inputs +static const PortDesc2 P2_BANG_IN[] = {{"bang_in", "trigger input", PortKind2::BangTrigger}}; +static const PortDesc2 P2_VALUE[] = {{"value", "input value"}}; + +// expr! inputs +static const PortDesc2 P2_EXPR_BANG_IN[] = {{"bang_in", "trigger input", PortKind2::BangTrigger}}; + +// store! inputs: bang, target, value +static const PortDesc2 P2_STORE_BANG_IN[] = { + {"bang_in", "trigger", PortKind2::BangTrigger}, + {"target", "variable/reference to store into"}, + {"value", "value to store"}, +}; + +// store (no bang) inputs: target, value +static const PortDesc2 P2_STORE_IN[] = { + {"target", "variable/reference to store into"}, + {"value", "value to store"}, +}; + +// append! inputs: bang, target, value +static const PortDesc2 P2_APPEND_BANG_IN[] = { + {"bang_in", "trigger", PortKind2::BangTrigger}, + {"target", "collection to append to"}, + {"value", "value to append"}, +}; + +// append (no bang) inputs: target, value +static const PortDesc2 P2_APPEND_IN[] = { + {"target", "collection to append to"}, + {"value", "value to append"}, +}; + +// erase inputs +static const PortDesc2 P2_ERASE_BANG_IN[] = { + {"bang_in", "trigger", PortKind2::BangTrigger}, + {"target", "collection to erase from"}, + {"key", "key/value/iterator to erase"}, +}; +static const PortDesc2 P2_ERASE_IN[] = { + {"target", "collection to erase from"}, + {"key", "key/value/iterator to erase"}, +}; + +// select inputs: condition, if_true, if_false +static const PortDesc2 P2_SELECT_IN[] = { + {"condition", "boolean selector"}, + {"if_true", "value when true"}, + {"if_false", "value when false"}, +}; + +// select! inputs: bang, condition +static const PortDesc2 P2_SELECT_BANG_IN[] = { + {"bang_in", "trigger", PortKind2::BangTrigger}, + {"condition", "boolean condition"}, +}; +// select! outputs: next, true, false +static const PortDesc2 P2_SELECT_BANG_OUT[] = { + {"next", "fires after branch completes", PortKind2::BangNext}, + {"true", "fires when true", PortKind2::BangNext}, + {"false", "fires when false", PortKind2::BangNext}, +}; + +// new: va_args fields +static const PortDesc2 P2_NEW_IN[] = { + {"field", "constructor field", PortKind2::Data, nullptr, true, true}, +}; + +// call: va_args arguments +static const PortDesc2 P2_CALL_IN[] = { + {"arg", "function argument", PortKind2::Data, nullptr, true, true}, +}; +// call!: bang + va_args +static const PortDesc2 P2_CALL_BANG_IN[] = { + {"bang_in", "trigger", PortKind2::BangTrigger}, + {"arg", "function argument", PortKind2::Data, nullptr, true, true}, +}; + +// iterate: collection + fn(lambda) +static const PortDesc2 P2_ITERATE_IN[] = { + {"collection", "collection to iterate over"}, + {"fn", "it=fn(it); while it!=end", PortKind2::Lambda}, +}; +static const PortDesc2 P2_ITERATE_BANG_IN[] = { + {"bang_in", "trigger", PortKind2::BangTrigger}, + {"collection", "collection to iterate over"}, + {"fn", "it=fn(it); while it!=end", PortKind2::Lambda}, +}; + +// lock: mutex + fn(lambda) + optional va_args params +static const PortDesc2 P2_LOCK_IN[] = { + {"mutex", "mutex to lock"}, + {"fn", "body under lock", PortKind2::Lambda}, + {"param", "lambda parameter", PortKind2::Data, nullptr, true, true}, +}; +static const PortDesc2 P2_LOCK_BANG_IN[] = { + {"bang_in", "trigger", PortKind2::BangTrigger}, + {"mutex", "mutex to lock"}, + {"fn", "body under lock", PortKind2::Lambda}, + {"param", "lambda parameter", PortKind2::Data, nullptr, true, true}, +}; + +// decl inputs +static const PortDesc2 P2_DECL_TYPE_IN[] = { + {"bang_in", "trigger", PortKind2::BangTrigger}, + {"name", "type name (symbol)"}, + {"type", "type definition"}, +}; +static const PortDesc2 P2_DECL_TYPE_OUT[] = { + {"next", "fires after declaration", PortKind2::BangNext}, + {"type", "the declared type"}, +}; +static const PortDesc2 P2_DECL_VAR_IN[] = { + {"bang_in", "trigger", PortKind2::BangTrigger}, + {"name", "variable name (symbol)"}, + {"type", "variable type"}, +}; +static const PortDesc2 P2_DECL_VAR_OUT[] = { + {"next", "fires after declaration", PortKind2::BangNext}, + {"ref", "reference to variable"}, +}; +static const PortDesc2 P2_DECL_OUT[] = {{"next", "fires to start declarations", PortKind2::BangNext}}; +static const PortDesc2 P2_DECL_EVENT_IN[] = { + {"bang_in", "trigger", PortKind2::BangTrigger}, + {"name", "event name (symbol)"}, + {"type", "event function type"}, +}; +static const PortDesc2 P2_DECL_IMPORT_IN[] = { + {"bang_in", "trigger", PortKind2::BangTrigger}, + {"path", "module path", PortKind2::Data, "literal"}, +}; +static const PortDesc2 P2_FFI_IN[] = { + {"bang_in", "trigger", PortKind2::BangTrigger}, + {"name", "function name (symbol)"}, + {"type", "function type"}, +}; + +// discard +static const PortDesc2 P2_DISCARD_BANG_IN[] = { + {"bang_in", "trigger", PortKind2::BangTrigger}, + {"value", "value to discard"}, +}; + +// output_mix! +static const PortDesc2 P2_OUTPUT_MIX_IN[] = { + {"bang_in", "trigger", PortKind2::BangTrigger}, + {"value", "audio sample to mix"}, +}; + +// resize! +static const PortDesc2 P2_RESIZE_IN[] = { + {"bang_in", "trigger", PortKind2::BangTrigger}, + {"target", "vector to resize"}, + {"size", "new size", PortKind2::Data, "s32"}, +}; + +// on_key outputs: next + data +static const PortDesc2 P2_KEY_OUT[] = { + {"next", "fires on key event", PortKind2::BangNext}, + {"midi_key", "MIDI note number", PortKind2::Data, "u8"}, + {"freq", "frequency in Hz", PortKind2::Data, "f32"}, +}; +// on_key_up outputs: next + midi_key only +static const PortDesc2 P2_KEY_UP_OUT[] = { + {"next", "fires on key release", PortKind2::BangNext}, + {"midi_key", "MIDI note number", PortKind2::Data, "u8"}, + {"freq", "frequency in Hz", PortKind2::Data, "f32"}, +}; + +// event! outputs +static const PortDesc2 P2_EVENT_OUT[] = {{"next", "fires on event", PortKind2::BangNext}}; + +// ─── Node type table ─── + +static const NodeType2 NODE_TYPES2[] = { + // expr: no fixed inputs, outputs = args count + {NodeTypeID::Expr, "expr", "Evaluate expression", + nullptr, 0, P2_RESULT, 1, false, false}, + + // select: 3 fixed inputs, 1 output + {NodeTypeID::Select, "select", "Select value by condition", + P2_SELECT_IN, 3, P2_RESULT, 1, false, false}, + + // new: va_args fields, 1 output + {NodeTypeID::New, "new", "Instantiate a type", + P2_NEW_IN, 1, P2_RESULT, 1, false, false}, + + // dup: 1 input, 1 output + {NodeTypeID::Dup, "dup", "Duplicate input to output", + P2_VALUE, 1, P2_RESULT, 1, false, false}, + + // str: 1 input, 1 output + {NodeTypeID::Str, "str", "Convert to string", + P2_VALUE, 1, P2_RESULT, 1, false, false}, + + // void: no inputs, 1 output + {NodeTypeID::Void, "void", "Void result", + nullptr, 0, P2_RESULT, 1, false, false}, + + // discard!: bang + value, next output + {NodeTypeID::DiscardBang, "discard!", "Discard value, pass bang", + P2_DISCARD_BANG_IN, 2, P2_NEXT, 1, false, false}, + + // discard: 1 input, no outputs + {NodeTypeID::Discard, "discard", "Discard input values", + P2_VALUE, 1, nullptr, 0, false, false}, + + // decl_type + {NodeTypeID::DeclType, "decl_type", "Declare a type", + P2_DECL_TYPE_IN, 3, P2_DECL_TYPE_OUT, 2, false, true}, + + // decl_var + {NodeTypeID::DeclVar, "decl_var", "Declare a variable", + P2_DECL_VAR_IN, 3, P2_DECL_VAR_OUT, 2, false, true}, + + // decl + {NodeTypeID::Decl, "decl", "Compile-time entry point", + nullptr, 0, P2_DECL_OUT, 1, false, true}, + + // decl_event + {NodeTypeID::DeclEvent, "decl_event", "Declare event", + P2_DECL_EVENT_IN, 3, P2_NEXT, 1, false, true}, + + // decl_import + {NodeTypeID::DeclImport, "decl_import","Import module", + P2_DECL_IMPORT_IN, 2, P2_NEXT, 1, false, true}, + + // ffi + {NodeTypeID::Ffi, "ffi", "Declare external function", + P2_FFI_IN, 3, P2_NEXT, 1, false, true}, + + // call: va_args, 1 output + {NodeTypeID::Call, "call", "Call function", + P2_CALL_IN, 1, P2_RESULT, 1, false, false}, + + // call!: bang + va_args, next + result + {NodeTypeID::CallBang, "call!", "Call function (bang)", + P2_CALL_BANG_IN, 2, P2_NEXT_RESULT, 2, false, false}, + + // erase: 2 inputs, 1 output + {NodeTypeID::Erase, "erase", "Erase from collection", + P2_ERASE_IN, 2, P2_RESULT, 1, false, false}, + + // output_mix! + {NodeTypeID::OutputMixBang, "output_mix!","Mix into audio output", + P2_OUTPUT_MIX_IN, 2, nullptr, 0, false, false}, + + // append: 2 inputs, 1 output + {NodeTypeID::Append, "append", "Append to collection", + P2_APPEND_IN, 2, P2_RESULT, 1, false, false}, + + // append!: bang + 2 inputs, next + result + {NodeTypeID::AppendBang, "append!", "Append to collection (bang)", + P2_APPEND_BANG_IN, 3, P2_NEXT_RESULT, 2, false, false}, + + // store: 2 inputs, no outputs + {NodeTypeID::Store, "store", "Store value", + P2_STORE_IN, 2, nullptr, 0, false, false}, + + // store!: bang + 2 inputs, next + {NodeTypeID::StoreBang, "store!", "Store value (bang)", + P2_STORE_BANG_IN, 3, P2_NEXT, 1, false, false}, + + // event!: no inputs, next output + {NodeTypeID::EventBang, "event!", "Event source", + nullptr, 0, P2_EVENT_OUT, 1, true, false}, + + // on_key_down!: no inputs, next + 2 data outputs + {NodeTypeID::OnKeyDownBang, "on_key_down!","Key press event", + nullptr, 0, P2_KEY_OUT, 3, true, false}, + + // on_key_up!: no inputs, next + 2 data outputs + {NodeTypeID::OnKeyUpBang, "on_key_up!", "Key release event", + nullptr, 0, P2_KEY_UP_OUT, 3, true, false}, + + // select!: bang + condition, 3 bang outputs + {NodeTypeID::SelectBang, "select!", "Branch on condition", + P2_SELECT_BANG_IN, 2, P2_SELECT_BANG_OUT, 3, false, false}, + + // expr!: bang input, next + outputs (dynamic) + {NodeTypeID::ExprBang, "expr!", "Evaluate expression on bang", + P2_EXPR_BANG_IN, 1, P2_NEXT, 1, false, false}, + + // erase!: bang + 2 inputs, next + result + {NodeTypeID::EraseBang, "erase!", "Erase from collection (bang)", + P2_ERASE_BANG_IN, 3, P2_NEXT_RESULT, 2, false, false}, + + // iterate: collection + fn, no outputs + {NodeTypeID::Iterate, "iterate", "Iterate collection", + P2_ITERATE_IN, 2, nullptr, 0, false, false}, + + // iterate!: bang + collection + fn, next + {NodeTypeID::IterateBang, "iterate!", "Iterate collection (bang)", + P2_ITERATE_BANG_IN, 3, P2_NEXT, 1, false, false}, + + // next: 1 input, 1 output + {NodeTypeID::Next, "next", "Advance iterator", + P2_VALUE, 1, P2_RESULT, 1, false, false}, + + // lock: mutex + fn + va_args, no outputs + {NodeTypeID::Lock, "lock", "Execute under mutex lock", + P2_LOCK_IN, 3, nullptr, 0, false, false}, + + // lock!: bang + mutex + fn + va_args, next + {NodeTypeID::LockBang, "lock!", "Execute under mutex lock (bang)", + P2_LOCK_BANG_IN, 4, P2_NEXT, 1, false, false}, + + // resize!: bang + target + size, next + {NodeTypeID::ResizeBang, "resize!", "Resize vector", + P2_RESIZE_IN, 3, P2_NEXT, 1, false, false}, + + // cast: 1 input, 1 output + {NodeTypeID::Cast, "cast", "Cast value to type", + P2_VALUE, 1, P2_RESULT, 1, false, false}, + + // label: no pins + {NodeTypeID::Label, "label", "Text label", + nullptr, 0, nullptr, 0, false, false}, + + // deref: 1 input, 1 output + {NodeTypeID::Deref, "deref", "Dereference iterator (internal)", + P2_VALUE, 1, P2_RESULT, 1, false, false}, + + // error: no pins + {NodeTypeID::Error, "error", "Error: invalid node", + nullptr, 0, nullptr, 0, false, false}, +}; + +static constexpr int NUM_NODE_TYPES2 = sizeof(NODE_TYPES2) / sizeof(NODE_TYPES2[0]); + +static const NodeType2* find_node_type2(NodeTypeID id) { + auto idx = static_cast(id); + if (idx < NUM_NODE_TYPES2) return &NODE_TYPES2[idx]; + return nullptr; +} + +static const NodeType2* find_node_type2(const char* name) { + for (int i = 0; i < NUM_NODE_TYPES2; i++) + if (strcmp(NODE_TYPES2[i].name, name) == 0) return &NODE_TYPES2[i]; + return nullptr; +} diff --git a/src/attoflow/editor.cpp b/src/attoflow/editor.cpp index 6ec968b..7b945a9 100644 --- a/src/attoflow/editor.cpp +++ b/src/attoflow/editor.cpp @@ -324,10 +324,18 @@ void FlowEditorWindow::open_tab(const std::string& file_path) { TabState tab; tab.file_path = abs_path; tab.tab_name = fs::path(file_path).stem().string(); + tab.use_editor2 = true; + if (fs::exists(abs_path)) { - load_atto(abs_path, tab.graph); + // Try loading via Editor2Pane first + if (!tab.editor2.load(abs_path)) { + // Fallback to legacy loader + tab.use_editor2 = false; + load_atto(abs_path, tab.graph); + } } - if (tab.graph.has_viewport) { + + if (!tab.use_editor2 && tab.graph.has_viewport) { tab.canvas_offset = {tab.graph.viewport_x, tab.graph.viewport_y}; tab.canvas_zoom = tab.graph.viewport_zoom; } @@ -886,6 +894,12 @@ void FlowEditorWindow::draw() { ImGui::BeginChild("##flow_canvas", {canvas_w, canvas_h}, false, ImGuiWindowFlags_NoScrollbar); + if (active().use_editor2) { + active().editor2.draw(); + ImGui::EndChild(); // flow_canvas + } else { + // === Legacy Editor1 canvas === + ImVec2 canvas_origin = ImGui::GetCursorScreenPos(); ImVec2 canvas_size = ImGui::GetContentRegionAvail(); ImDrawList* dl = ImGui::GetWindowDrawList(); @@ -2007,7 +2021,8 @@ void FlowEditorWindow::draw() { } } - ImGui::EndChild(); // flow_canvas + ImGui::EndChild(); // flow_canvas (legacy) + } // end legacy Editor1 canvas // --- Horizontal splitter (between canvas and bottom panel) --- ImGui::InvisibleButton("##hsplitter", {canvas_w, 4.0f}); diff --git a/src/attoflow/editor.h b/src/attoflow/editor.h index 1e1440b..67f1dc4 100644 --- a/src/attoflow/editor.h +++ b/src/attoflow/editor.h @@ -2,6 +2,7 @@ #include "sdl_imgui_window.h" #include "atto/model.h" #include "atto/types.h" +#include "editor2.h" #include #include #include @@ -19,7 +20,9 @@ inline Vec2 to_vec2(ImVec2 v) { return {v.x, v.y}; } // Per-tab state: each open .atto file gets its own TabState struct TabState { - FlowGraph graph; + FlowGraph graph; // legacy (Editor1) + Editor2Pane editor2; // new editor pane + bool use_editor2 = true; // true = use Editor2Pane, false = legacy std::string file_path; // absolute path to this .atto file std::string tab_name; // display name (filename without extension) bool dirty = false; diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp new file mode 100644 index 0000000..2284516 --- /dev/null +++ b/src/attoflow/editor2.cpp @@ -0,0 +1,233 @@ +#include "editor2.h" +#include "atto/graphbuilder.h" +#include "atto/node_types2.h" +#include "imgui.h" +#include +#include +#include +#include +#include + +// ─── Constants ─── + +static constexpr float NODE_MIN_WIDTH = 80.0f; +static constexpr float NODE_HEIGHT = 40.0f; +static constexpr float PIN_RADIUS = 5.0f; +static constexpr float PIN_SPACING = 16.0f; + +static constexpr ImU32 COL_BG = IM_COL32(30, 30, 40, 255); +static constexpr ImU32 COL_GRID = IM_COL32(50, 50, 60, 255); +static constexpr ImU32 COL_NODE = IM_COL32(50, 55, 75, 220); +static constexpr ImU32 COL_NODE_SEL = IM_COL32(80, 90, 130, 255); +static constexpr ImU32 COL_NODE_ERR = IM_COL32(130, 40, 40, 220); +static constexpr ImU32 COL_TEXT = IM_COL32(220, 220, 220, 255); +static constexpr ImU32 COL_PIN_DATA = IM_COL32(100, 200, 100, 255); +static constexpr ImU32 COL_PIN_BANG = IM_COL32(255, 200, 80, 255); +static constexpr ImU32 COL_PIN_LAMBDA= IM_COL32(180, 130, 255, 255); +static constexpr ImU32 COL_LINK = IM_COL32(200, 200, 100, 200); + +// ─── Helpers ─── + +static inline ImVec2 v2add(ImVec2 a, ImVec2 b) { return {a.x + b.x, a.y + b.y}; } +static inline ImVec2 v2sub(ImVec2 a, ImVec2 b) { return {a.x - b.x, a.y - b.y}; } +static inline ImVec2 v2mul(ImVec2 a, float s) { return {a.x * s, a.y * s}; } + +static ImU32 pin_color(PortKind2 kind) { + switch (kind) { + case PortKind2::BangTrigger: + case PortKind2::BangNext: return COL_PIN_BANG; + case PortKind2::Lambda: return COL_PIN_LAMBDA; + default: return COL_PIN_DATA; + } +} + +// ─── Load ─── + +bool Editor2Pane::load(const std::string& path) { + std::ifstream f(path); + if (!f.is_open()) { + fprintf(stderr, "Editor2: cannot open %s\n", path.c_str()); + return false; + } + + auto result = Deserializer::parse_atto(f); + if (auto* err = std::get_if(&result)) { + fprintf(stderr, "Editor2: %s\n", err->c_str()); + return false; + } + + gb_ = std::get>(result); + file_path_ = path; + + // Extract tab name from path + auto slash = path.find_last_of("/\\"); + tab_name_ = (slash != std::string::npos) ? path.substr(slash + 1) : path; + + printf("Editor2: loaded %zu entries from %s\n", gb_->entries.size(), path.c_str()); + return true; +} + +// ─── Draw ─── + +void Editor2Pane::draw() { + if (!gb_) { + ImGui::TextDisabled("No file loaded"); + return; + } + + ImVec2 canvas_p0 = ImGui::GetCursorScreenPos(); + ImVec2 canvas_sz = ImGui::GetContentRegionAvail(); + if (canvas_sz.x < 50.0f) canvas_sz.x = 50.0f; + if (canvas_sz.y < 50.0f) canvas_sz.y = 50.0f; + + ImGui::InvisibleButton("##canvas2", canvas_sz, + ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight); + bool canvas_hovered = ImGui::IsItemHovered(); + + ImDrawList* dl = ImGui::GetWindowDrawList(); + + // Background + dl->AddRectFilled(canvas_p0, v2add(canvas_p0, canvas_sz), COL_BG); + + ImVec2 canvas_origin = v2add(canvas_p0, canvas_offset_); + + // Grid + float grid_step = 20.0f * canvas_zoom_; + if (grid_step > 5.0f) { + for (float x = fmodf(canvas_offset_.x, grid_step); x < canvas_sz.x; x += grid_step) + dl->AddLine({canvas_p0.x + x, canvas_p0.y}, {canvas_p0.x + x, canvas_p0.y + canvas_sz.y}, COL_GRID); + for (float y = fmodf(canvas_offset_.y, grid_step); y < canvas_sz.y; y += grid_step) + dl->AddLine({canvas_p0.x, canvas_p0.y + y}, {canvas_p0.x + canvas_sz.x, canvas_p0.y + y}, COL_GRID); + } + + // Clip + dl->PushClipRect(canvas_p0, v2add(canvas_p0, canvas_sz), true); + + // Draw nodes + for (auto& [id, entry] : gb_->entries) { + if (std::holds_alternative(*entry)) { + draw_node(dl, id, std::get(*entry), canvas_origin); + } + } + + // Draw nets (wires) + // TODO: draw connections between nodes via nets + + dl->PopClipRect(); + + // Pan with middle mouse or right mouse + if (canvas_hovered && ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) { + canvas_offset_.x += ImGui::GetIO().MouseDelta.x; + canvas_offset_.y += ImGui::GetIO().MouseDelta.y; + } + if (canvas_hovered && ImGui::IsMouseDragging(ImGuiMouseButton_Right)) { + canvas_offset_.x += ImGui::GetIO().MouseDelta.x; + canvas_offset_.y += ImGui::GetIO().MouseDelta.y; + } + + // Zoom with scroll + if (canvas_hovered) { + float wheel = ImGui::GetIO().MouseWheel; + if (wheel != 0) { + float old_zoom = canvas_zoom_; + canvas_zoom_ *= (wheel > 0) ? 1.1f : 0.9f; + canvas_zoom_ = std::clamp(canvas_zoom_, 0.1f, 10.0f); + // Zoom toward mouse position + ImVec2 mouse = ImGui::GetIO().MousePos; + ImVec2 mouse_rel = v2sub(v2sub(mouse, canvas_p0), canvas_offset_); + ImVec2 mouse_canvas = v2mul(mouse_rel, 1.0f / old_zoom); + canvas_offset_ = v2sub(v2sub(mouse, canvas_p0), v2mul(mouse_canvas, canvas_zoom_)); + } + } +} + +// ─── Draw a node ─── + +void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuilder& node, + ImVec2 canvas_origin) { + auto* nt = find_node_type2(node.type_id); + if (!nt) return; + + // Compute display text + std::string display = nt->name; + std::string args = node.args_str(); + if (!args.empty()) display += " " + args; + + // Node rect + float font_size = ImGui::GetFontSize() * canvas_zoom_; + ImVec2 text_sz = ImGui::CalcTextSize(display.c_str()); + float text_w = text_sz.x * canvas_zoom_ + 16.0f * canvas_zoom_; + + // Pin counts + int num_in = nt->num_inputs + (node.parsed_args ? node.parsed_args->rewrite_input_count : 0); + int num_out = nt->num_outputs; + if (node.type_id == NodeTypeID::Expr || node.type_id == NodeTypeID::ExprBang) { + int args_count = node.parsed_args ? (int)node.parsed_args->size() : 0; + if (node.type_id == NodeTypeID::ExprBang) num_out = 1 + std::max(1, args_count); // next + outputs + else num_out = std::max(1, args_count); + } + + float pin_w_top = std::max(0, num_in) * PIN_SPACING * canvas_zoom_; + float pin_w_bot = std::max(0, num_out) * PIN_SPACING * canvas_zoom_; + float node_w = std::max({NODE_MIN_WIDTH * canvas_zoom_, text_w, pin_w_top, pin_w_bot}); + float node_h = NODE_HEIGHT * canvas_zoom_; + + ImVec2 pos = {canvas_origin.x + node.position.x * canvas_zoom_, + canvas_origin.y + node.position.y * canvas_zoom_}; + + bool selected = selected_nodes_.count(id); + bool has_error = !node.error.empty(); + + ImU32 col = has_error ? COL_NODE_ERR : (selected ? COL_NODE_SEL : COL_NODE); + dl->AddRectFilled(pos, {pos.x + node_w, pos.y + node_h}, col, 4.0f * canvas_zoom_); + dl->AddRect(pos, {pos.x + node_w, pos.y + node_h}, IM_COL32(80, 80, 100, 255), 4.0f * canvas_zoom_); + + // Text + if (font_size > 5.0f) { + float tw = text_sz.x * canvas_zoom_; + float cx = pos.x + (node_w - tw) * 0.5f; + float cy = pos.y + (node_h - font_size) * 0.5f; + dl->AddText(nullptr, font_size, {cx, cy}, COL_TEXT, display.c_str()); + } + + // Draw input pins (top) + float pr = PIN_RADIUS * canvas_zoom_; + for (int i = 0; i < num_in; i++) { + float px = pos.x + (i + 0.5f) * PIN_SPACING * canvas_zoom_; + float py = pos.y; + + PortKind2 kind = PortKind2::Data; + if (nt->input_ports && i < nt->num_inputs) kind = nt->input_ports[i].kind; + + ImU32 pc = pin_color(kind); + if (kind == PortKind2::BangTrigger) { + dl->AddRectFilled({px - pr, py - pr}, {px + pr, py + pr}, pc); + } else if (kind == PortKind2::Lambda) { + // Triangle for lambda + dl->AddTriangleFilled({px - pr, py - pr}, {px + pr, py}, {px - pr, py + pr}, pc); + } else { + dl->AddCircleFilled({px, py}, pr, pc); + } + } + + // Draw output pins (bottom) + for (int i = 0; i < num_out; i++) { + float px = pos.x + (i + 0.5f) * PIN_SPACING * canvas_zoom_; + float py = pos.y + node_h; + + PortKind2 kind = PortKind2::Data; + if (nt->output_ports && i < nt->num_outputs) kind = nt->output_ports[i].kind; + + ImU32 pc = pin_color(kind); + if (kind == PortKind2::BangNext) { + dl->AddRectFilled({px - pr, py - pr}, {px + pr, py + pr}, pc); + } else { + dl->AddCircleFilled({px, py}, pr, pc); + } + } +} + +void Editor2Pane::draw_net(ImDrawList* dl, const NodeId& id, const NetBuilder& net, + ImVec2 canvas_origin) { + // TODO: draw wires between connected nodes +} diff --git a/src/attoflow/editor2.h b/src/attoflow/editor2.h new file mode 100644 index 0000000..4dafd37 --- /dev/null +++ b/src/attoflow/editor2.h @@ -0,0 +1,43 @@ +#pragma once +#include "atto/graphbuilder.h" +#include "atto/node_types2.h" +#include "imgui.h" +#include +#include +#include + +// Editor2Pane: new editor using GraphBuilder exclusively. +// No FlowGraph, no inference, no codegen. +class Editor2Pane { +public: + // Load an .atto file (instrument@atto:0 format) + bool load(const std::string& path); + + // Draw the pane into the current ImGui context + void draw(); + + bool is_loaded() const { return gb_ != nullptr; } + bool is_dirty() const { return dirty_; } + const std::string& file_path() const { return file_path_; } + const std::string& tab_name() const { return tab_name_; } + +private: + std::shared_ptr gb_; + std::string file_path_; + std::string tab_name_; + bool dirty_ = false; + + // Canvas state + ImVec2 canvas_offset_ = {0, 0}; + float canvas_zoom_ = 1.0f; + + // Interaction state + std::set selected_nodes_; + int editing_link_id_ = -1; // not used yet, placeholder + + // Drawing helpers + void draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuilder& node, + ImVec2 canvas_origin); + void draw_net(ImDrawList* dl, const NodeId& id, const NetBuilder& net, + ImVec2 canvas_origin); +}; From 925b80857d49a433cce254acf4421a7d6bc2073a Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 01:37:09 +0200 Subject: [PATCH 14/86] Folding --- src/atto/graphbuilder.cpp | 111 ++++++++++++++++++++++++++++++++++++-- src/atto/graphbuilder.h | 10 +++- src/attoflow/editor2.cpp | 7 ++- 3 files changed, 120 insertions(+), 8 deletions(-) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index cca4ac6..64917f1 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -201,6 +201,15 @@ FlowNodeBuilder& GraphBuilder::add_node(NodeId id, NodeTypeID type, std::shared_ return nb; } +void GraphBuilder::ensure_unconnected() { + if (entries.count("$unconnected")) return; + auto entry = std::make_shared(NetBuilder{}); + auto& net = std::get(*entry); + net.is_the_unconnected = true; + net.auto_wire = true; + entries["$unconnected"] = entry; +} + std::pair GraphBuilder::find_or_create_net(const NodeId& name, bool for_source) { auto [id, ptr] = find_net(name); // throws if name exists as a node if (ptr) { @@ -238,12 +247,14 @@ std::pair GraphBuilder::find_net(const NodeId& name) { void GraphBuilder::compact() { for (auto it = entries.begin(); it != entries.end(); ) { - if (std::holds_alternative(*it->second) && - std::get(*it->second).unused()) { - it = entries.erase(it); - } else { - ++it; + if (std::holds_alternative(*it->second)) { + auto& net = std::get(*it->second); + if (!net.is_the_unconnected && net.unused()) { + it = entries.erase(it); + continue; + } } + ++it; } } @@ -322,6 +333,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { } auto gb = std::make_shared(); + gb->ensure_unconnected(); bool in_node = false; std::string cur_id, cur_type; @@ -330,6 +342,9 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { float cur_x = 0, cur_y = 0; bool cur_shadow = false; + // Track shadow input nets for remap construction during folding + std::map> shadow_input_nets; // shadow_id → input net names + auto flush_node = [&]() { if (cur_type.empty()) { cur_id.clear(); cur_args.clear(); cur_inputs.clear(); cur_outputs.clear(); @@ -344,6 +359,11 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { nb.position = {cur_x, cur_y}; nb.shadow = cur_shadow; + // Save shadow input nets for later folding + if (cur_shadow) { + shadow_input_nets[cur_id] = cur_inputs; + } + auto node_entry = gb->find(cur_id); // Wire nets from outputs (this node is source) @@ -399,5 +419,86 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { } flush_node(); + // ─── Fold shadow nodes into parents ─── + auto unconnected_entry = gb->find("$unconnected"); + + // Collect shadow ids + std::vector shadow_ids; + for (auto& [id, entry] : gb->entries) { + if (!std::holds_alternative(*entry)) continue; + if (std::get(*entry).shadow) + shadow_ids.push_back(id); + } + + for (auto& shadow_id : shadow_ids) { + // Extract parent id and arg index: "$auto-xyz_s0" → "$auto-xyz", 0 + auto underscore_s = shadow_id.rfind("_s"); + if (underscore_s == std::string::npos) continue; + std::string parent_id = shadow_id.substr(0, underscore_s); + int arg_index = std::stoi(shadow_id.substr(underscore_s + 2)); + + auto [_, parent_ptr] = gb->find_node(parent_id); + if (!parent_ptr) continue; + auto& parent = std::get(*parent_ptr); + + auto shadow_ptr = gb->find(shadow_id); + if (!shadow_ptr) continue; + auto& shadow = std::get(*shadow_ptr); + + // Insert shadow expression into parent's parsed_args at arg_index + if (parent.parsed_args) { + while ((int)parent.parsed_args->size() <= arg_index) + parent.parsed_args->push_back(ArgString2{""}); + if (shadow.parsed_args && !shadow.parsed_args->empty()) + (*parent.parsed_args)[arg_index] = (*shadow.parsed_args)[0]; + } + + // Build remaps from saved shadow input nets + auto sin_it = shadow_input_nets.find(shadow_id); + if (sin_it != shadow_input_nets.end()) { + auto& sin = sin_it->second; + // Shadow is an expr — no triggers, so inputs[i] maps directly to $i + for (int i = 0; i < (int)sin.size(); i++) { + // Ensure parent remaps is large enough + while ((int)parent.remaps.size() <= i) + parent.remaps.push_back(ArgNet2{"$unconnected", unconnected_entry}); + + if (!sin[i].empty()) { + auto net_ptr = gb->find(sin[i]); + parent.remaps[i] = ArgNet2{sin[i], net_ptr}; + + // Remove shadow from net's destinations, add parent instead + if (net_ptr && std::holds_alternative(*net_ptr)) { + auto& net = std::get(*net_ptr); + // Remove shadow destinations + auto& dests = net.destinations; + dests.erase( + std::remove_if(dests.begin(), dests.end(), + [&](auto& w) { return w.lock() == shadow_ptr; }), + dests.end()); + // Add parent as destination + net.destinations.push_back(parent_ptr); + } + } + } + } + + // Remove nets where shadow is source (internal shadow→parent plumbing) + std::vector nets_to_remove; + for (auto& [net_id, net_entry] : gb->entries) { + if (!std::holds_alternative(*net_entry)) continue; + auto src = std::get(*net_entry).source.lock(); + if (src == shadow_ptr) + nets_to_remove.push_back(net_id); + } + for (auto& nid : nets_to_remove) + gb->entries.erase(nid); + + // Remove shadow from graph + gb->entries.erase(shadow_id); + } + + gb->compact(); + return gb; } diff --git a/src/atto/graphbuilder.h b/src/atto/graphbuilder.h index 2cce793..33617bb 100644 --- a/src/atto/graphbuilder.h +++ b/src/atto/graphbuilder.h @@ -53,6 +53,7 @@ std::string reconstruct_args_str(const ParsedArgs2& args); // Named wire — one source, many destinations (weak refs to BuilderEntry, must be FlowNodeBuilder). struct NetBuilder { bool auto_wire = false; + bool is_the_unconnected = false; // true for the special $unconnected sentinel BuilderEntryWeak source; std::vector destinations; @@ -62,12 +63,16 @@ struct NetBuilder { void validate() const; }; +// Remap: $N → net mapping (from folded shadow inputs) +using Remaps = std::vector; + // A node under construction — holds structured parsed args instead of raw string. struct FlowNodeBuilder { NodeTypeID type_id = NodeTypeID::Unknown; std::shared_ptr parsed_args; + Remaps remaps; // $N → net mapping (remaps[0] = net for $0, etc.) Vec2 position = {0, 0}; - bool shadow = false; + bool shadow = false; // only used during migration, must be false after folding std::string error; std::string args_str() const; @@ -81,6 +86,9 @@ struct GraphBuilder { FlowNodeBuilder& add_node(NodeId id, NodeTypeID type, std::shared_ptr args); + // Ensure the $unconnected sentinel net exists + void ensure_unconnected(); + std::pair find_or_create_net(const NodeId& name, bool for_source = false); BuilderEntryPtr find(const NodeId& id); diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 2284516..b9dfc5e 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -7,6 +7,7 @@ #include #include #include +#include // ─── Constants ─── @@ -103,10 +104,12 @@ void Editor2Pane::draw() { // Clip dl->PushClipRect(canvas_p0, v2add(canvas_p0, canvas_sz), true); - // Draw nodes + // Draw nodes (skip shadows) for (auto& [id, entry] : gb_->entries) { if (std::holds_alternative(*entry)) { - draw_node(dl, id, std::get(*entry), canvas_origin); + auto& node = std::get(*entry); + if (node.shadow) throw std::logic_error("Editor2Pane: shadow nodes must be folded before rendering (id: " + id + ")"); + draw_node(dl, id, node, canvas_origin); } } From 099e4432412e45abd01c0530495c262f05e44d67 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 02:17:42 +0200 Subject: [PATCH 15/86] attolang.md update --- docs/attolang.md | 194 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 148 insertions(+), 46 deletions(-) diff --git a/docs/attolang.md b/docs/attolang.md index 61f58cd..150b9ff 100644 --- a/docs/attolang.md +++ b/docs/attolang.md @@ -457,27 +457,31 @@ Nodes with input or output bangs: ## Inline Expressions -All non-declaration nodes support **inline expressions** in their arguments. Each space-separated arg token replaces the corresponding descriptor input. If an arg is an inline expression (a literal, symbol, or complex expression), that input slot is "filled" and does not require a pin connection. Only `$N` references within inline expressions create actual input pins. +All non-declaration nodes support **inline expressions** in their arguments. Each arg maps 1:1 to a descriptor input port. An arg can be: + +- **Net reference** (`$net-name`): connects to a named net — produces a visible input pin +- **Expression** (`sin($0)+1`): inline expression — displayed in node text, no pin +- **Literal** (`42`, `"hello"`, `true`): inline constant — displayed in node text, no pin +- **Symbol** (`oscs`, `sin`): bare identifier resolved via symbol table — displayed in text, no pin + +Only net references produce visible input pins. All other arg types fill the slot inline. ### Rules -1. Each arg token (space-separated, respecting parentheses and quotes) maps to a descriptor input left-to-right -2. The number of arg tokens must not exceed the node's descriptor input count (error otherwise) -3. `$N` references within inline args create input pins; symbol references (bare names) do not -4. Pin indices must be contiguous starting from 0 — gaps (e.g. `$0` and `$2` without `$1`) are errors -5. Descriptor inputs beyond the number of inline args remain as pin connections +1. Each arg maps to a descriptor input left-to-right +2. The number of args must not exceed the node's descriptor input count (plus va-args if applicable) +3. `$N` references within expressions create **remap pins** (mapped via the `remaps` array) +4. Remap indices must be contiguous starting from 0 — gaps produce errors +5. `$name` (non-numeric, starting with `$`) references a named net and produces a visible pin +6. Bare names (no `$` prefix) are symbols resolved via the symbol table, not pins -### Examples (store! has 2 descriptor inputs: target, value) +### Examples (store! has 3 descriptor inputs: bang_in, target, value) -| Node text | Pins | Explanation | -|-----------|------|-------------| -| `store!` | target, value | No inline args — both inputs are pins | -| `store! oscs` | value | target filled by symbol `oscs` (resolves via symbol table to `&T`) | -| `store! oscs 42` | (none) | Both filled inline | -| `store! oscs $0` | $0 | target = symbol, value = pin $0 | -| `store! $1 $0` | $0, $1 | Both inline but reference pins | -| `store! $0 $1 $2` | error | Too many args (store! takes 2) | -| `store! $0 $2` | error | Missing pin $1 | +| Args | Visible pins | Explanation | +|------|-------------|-------------| +| `["$bang-src", "$var-ref", "$val-net"]` | 3 pins | All net refs — all visible | +| `["$bang-src", "oscs", "$0"]` | 2 pins (bang + remap $0) | target filled by symbol `oscs` | +| `["$bang-src", "oscs", "42"]` | 1 pin (bang only) | Both target and value filled inline | ## Expression Language @@ -627,8 +631,7 @@ When a lambda's data dependency traces back to a node in the caller scope, that Bang pins represent `() -> void` callable connections for control flow: - **BangTrigger** (top square): The node's callable entry point. When invoked, the node executes. Typed as `() -> void`. Can be used as a value source — connecting a BangTrigger to a data Input passes the `() -> void` callable as a value (e.g., to store it in a variable). -- **BangNext** (bottom square): The node's continuation. After execution, the node calls whatever is connected here. Typed as `() -> void`. Links go FROM BangNext TO BangTrigger. -- **Post-bang** (side): Fires after the node's inline expressions are evaluated. Same semantics as BangNext. +- **BangNext** (bottom square): The node's continuation output. After execution, the node calls whatever is connected here. Typed as `() -> void`. Links go FROM BangNext TO BangTrigger. The first output pin on bang nodes is always a `BangNext` named `next` — this replaces the old `post_bang` pseudo-pin and is rendered at the same visual position. **Link direction:** BangNext → BangTrigger. The "next" pin calls the "trigger" pin. @@ -636,44 +639,143 @@ Bang pins represent `() -> void` callable connections for control flow: **Bidirectional BangTrigger:** A BangTrigger pin can be both a link destination (receiving bang chain flow from BangNext) and a link source (providing its `() -> void` value to a data Input pin). -## File Format (.atto) +## File Format (instrument@atto:0) -TOML-like format: +TOML-like format with named nets instead of explicit pin-to-pin connections. ``` -version = "attoprog@0" - -[viewport] -x = -500.0 -y = -200.0 -zoom = 1.5 +# version instrument@atto:0 [[node]] -guid = "a3f7c1b2e9d04856" +id = "$gen-expr" type = "expr" -args = ["$0+$1"] -position = [100, 200] -connections = ["a3f7c1b2e9d04856.out0->b4c8d9e0f1a23456.0"] +args = ["sin($0.p)*$1/32.f"] +remaps = ["$iter-item", "$iter-amp"] +position = [1866.25, 1443.11] + +[[node]] +id = "$store-p" +type = "store!" +args = ["$0.p"] +remaps = ["$iter-item"] +position = [2133.84, 1421.55] +``` + +### Version Header + +First line: `# version instrument@atto:0` (comment-style). + +Legacy formats (`nanoprog@0`, `nanoprog@1`, `attoprog@0`, `attoprog@1`) are loaded via a legacy parser and auto-migrated. Saving always writes `instrument@atto:0`. + +### Node IDs and Net Names + +Node IDs and net names share the same namespace: +- Format: `$[a-zA-Z_-][a-zA-Z0-9_-]*` +- Auto-generated: `$auto-` for unnamed entries +- `$0`, `$1`, ... `$N` are reserved for expression pin inputs +- `$unconnected` is a reserved sentinel for unconnected pins +- The `$` prefix is stored in the file + +### Node Structure + +```toml +[[node]] +id = "$my-node" # human-readable identifier +type = "store!" # node type name +args = ["oscs", "$0"] # inline arguments (expressions, literals, net refs) +remaps = ["$data-net"] # $N → net mapping for expression pin inputs +position = [100, 200] # canvas coordinates +``` + +### Arguments (`args`) + +Each entry in the `args` array is a singular expression (space-delimited in the source, already split in the file). Arguments map 1:1 to the node's descriptor input ports. + +An argument can be: +- **Net reference** (`$name`): connects to a named net — produces a visible input pin +- **Expression** (`sin($0)+1`): inline expression with `$N` pin refs — displayed in node text, not a pin +- **Number** (`42`, `3.14f`): inline constant — displayed in node text +- **String** (`"hello"`): inline string literal — displayed in node text + +Only `ArgNet2` (net reference) entries produce visible input pins. Inline values are displayed in the node's label text. + +### Remaps (`remaps`) + +The `remaps` array maps `$N` expression pin inputs to named nets: + +```toml +remaps = ["$iter-item", "$iter-amp"] ``` -### Viewport Section +- `remaps[0]` = net for `$0`, `remaps[1]` = net for `$1`, etc. +- `$unconnected` for unconnected expression inputs +- Remaps are always net references -The optional `[viewport]` section stores the editor's camera state. It must appear after `version` and before any `[[node]]` entries. +### Pin Model -| Field | Type | Description | -|--------|-------|--------------------------------| -| `x` | float | Horizontal scroll offset | -| `y` | float | Vertical scroll offset | -| `zoom` | float | Zoom level (1.0 = default) | +#### Input pins (top of node, left to right) -### Connection Format +Only net reference (`$name`) arguments produce visible pins. The visible pin count is: -Connections use pin IDs: `".->."` +| Section | Visible pins | Source | +|---|---|---| +| **Base args** | Only net refs in `args` | 1:1 with descriptor input ports | +| **Va-args** | Only net refs in va-args | Named `{template}_0`, `_1`, ... | +| **Remaps** | All entries | `$0`, `$1`, ... from expressions | + +#### Output pins (bottom of node) + +All descriptor output ports are visible. Exception: `expr`/`expr!` output count equals `args` count. + +#### Pin kinds + +| Kind | Visual | Description | +|---|---|---| +| `BangTrigger` | Square (top) | Trigger input | +| `Data` | Circle | Data value | +| `Lambda` | Triangle | Lambda capture (accepts node refs or net refs) | +| `BangNext` | Square (bottom) | Bang continuation output | + +### Lambda Captures via Node ID + +When a `$id` in an argument resolves to a **node** (not a net), it is a lambda capture. The referenced node's subgraph becomes the lambda body. + +- `find_node(id)` → lambda capture +- `find_net(id)` → data wire +- Both `Lambda` and `Data` pins can accept lambda captures +- `Lambda` pins can ONLY accept lambdas (node refs) + +### Va-args + +Some node types accept a variable number of additional inputs (e.g., `new` for struct fields, `call` for function arguments, `lock` for lambda parameters). The va-args template is defined on the node type descriptor. Va-args pins are named `{template_name}_0`, `{template_name}_1`, etc. + +| Node | Va-args template | Description | +|---|---|---| +| `new` | `field` | Constructor fields (`field_0`, `field_1`, ...) | +| `call` / `call!` | `arg` | Function arguments (`arg_0`, `arg_1`, ...) | +| `lock` / `lock!` | `param` | Lambda parameters (`param_0`, `param_1`, ...) | + +### Viewport (Meta File) + +Viewport state is stored in `.atto/.yaml`, not in the `.atto` file: + +```yaml +# Editor metadata for main.atto +viewport_x: -1504.32 +viewport_y: -551.573 +viewport_zoom: 4.17725 +``` + +The `.atto/` directory is gitignored. Node positions remain in the `.atto` file. + +### Labels and Errors + +```toml +[[node]] +id = "$lbl-types" +type = "label" +args = ["Types"] +position = [766, 335] +``` -Pin names: -- Data/lambda inputs: `0`, `1`, `2`, ... or named (e.g. `gen`, `stop`) -- Bang inputs: `bang_in0`, `bang_in1`, ... -- Data outputs: `out0`, `out1`, ... -- Bang outputs: `bang0`, `bang1`, ... -- Lambda grab: `as_lambda` -- Post-bang: `post_bang` +Labels have exactly 1 argument (the display text). Error nodes are the same — they display the original args when parsing failed. From bf4d01f1519a48d6a1e7efe4ea51d5409a0639c3 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 02:18:21 +0200 Subject: [PATCH 16/86] towards editor2 --- src/atto/graphbuilder.cpp | 66 +++++++++--- src/atto/graphbuilder.h | 3 +- src/atto/node_types2.h | 43 ++++---- src/attoflow/editor2.cpp | 216 +++++++++++++++++++++++++++++--------- 4 files changed, 245 insertions(+), 83 deletions(-) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index 64917f1..616afbc 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -1,4 +1,5 @@ #include "graphbuilder.h" +#include "node_types2.h" #include #include #include @@ -186,8 +187,16 @@ std::string reconstruct_args_str(const ParsedArgs2& args) { // ─── FlowNodeBuilder ─── std::string FlowNodeBuilder::args_str() const { - if (!parsed_args) return ""; - return reconstruct_args_str(*parsed_args); + std::string result; + if (parsed_args) result = reconstruct_args_str(*parsed_args); + if (parsed_va_args && !parsed_va_args->empty()) { + std::string va = reconstruct_args_str(*parsed_va_args); + if (!va.empty()) { + if (!result.empty()) result += " "; + result += va; + } + } + return result; } // ─── GraphBuilder ─── @@ -465,22 +474,27 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if (!sin[i].empty()) { auto net_ptr = gb->find(sin[i]); - parent.remaps[i] = ArgNet2{sin[i], net_ptr}; - - // Remove shadow from net's destinations, add parent instead - if (net_ptr && std::holds_alternative(*net_ptr)) { - auto& net = std::get(*net_ptr); - // Remove shadow destinations - auto& dests = net.destinations; - dests.erase( - std::remove_if(dests.begin(), dests.end(), - [&](auto& w) { return w.lock() == shadow_ptr; }), - dests.end()); - // Add parent as destination - net.destinations.push_back(parent_ptr); + if (net_ptr) { + parent.remaps[i] = ArgNet2{sin[i], net_ptr}; + + // Remove shadow from net's destinations, add parent instead + if (std::holds_alternative(*net_ptr)) { + auto& net = std::get(*net_ptr); + auto& dests = net.destinations; + dests.erase( + std::remove_if(dests.begin(), dests.end(), + [&](auto& w) { return w.lock() == shadow_ptr; }), + dests.end()); + net.destinations.push_back(parent_ptr); + } } } } + // Update parent's rewrite_input_count to be the max across all shadows + if (parent.parsed_args) { + parent.parsed_args->rewrite_input_count = std::max( + parent.parsed_args->rewrite_input_count, (int)parent.remaps.size()); + } } // Remove nets where shadow is source (internal shadow→parent plumbing) @@ -498,6 +512,28 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { gb->entries.erase(shadow_id); } + // ─── Split parsed_args into base + va_args for nodes with va_args ─── + for (auto& [id, entry] : gb->entries) { + if (!std::holds_alternative(*entry)) continue; + auto& node = std::get(*entry); + auto* nt = find_node_type2(node.type_id); + if (!nt || !nt->va_args || !node.parsed_args) continue; + + // Count non-bang fixed inputs (args don't include bang triggers) + int fixed_args = 0; + for (int i = 0; i < nt->num_inputs; i++) { + if (nt->input_ports && nt->input_ports[i].kind != PortKind2::BangTrigger) + fixed_args++; + } + + if ((int)node.parsed_args->size() > fixed_args) { + node.parsed_va_args = std::make_shared(); + for (int i = fixed_args; i < (int)node.parsed_args->size(); i++) + node.parsed_va_args->push_back(std::move((*node.parsed_args)[i])); + node.parsed_args->resize(fixed_args); + } + } + gb->compact(); return gb; diff --git a/src/atto/graphbuilder.h b/src/atto/graphbuilder.h index 33617bb..4e3bd7c 100644 --- a/src/atto/graphbuilder.h +++ b/src/atto/graphbuilder.h @@ -69,7 +69,8 @@ using Remaps = std::vector; // A node under construction — holds structured parsed args instead of raw string. struct FlowNodeBuilder { NodeTypeID type_id = NodeTypeID::Unknown; - std::shared_ptr parsed_args; + std::shared_ptr parsed_args; // base pins (1:1 with descriptor) + std::shared_ptr parsed_va_args; // va_args pins Remaps remaps; // $N → net mapping (remaps[0] = net for $0, etc.) Vec2 position = {0, 0}; bool shadow = false; // only used during migration, must be false after folding diff --git a/src/atto/node_types2.h b/src/atto/node_types2.h index 7305423..94951df 100644 --- a/src/atto/node_types2.h +++ b/src/atto/node_types2.h @@ -16,7 +16,6 @@ struct PortDesc2 { PortKind2 kind = PortKind2::Data; const char* type_name = nullptr; bool optional = false; - bool va_args = false; // last pin only: repeats as name_0, name_1, ... }; struct NodeType2 { @@ -29,6 +28,7 @@ struct NodeType2 { int num_outputs; bool is_event = false; bool is_declaration = false; + const PortDesc2* va_args = nullptr; // nullptr = no va_args, else template for repeating pins }; // ─── Port descriptor arrays ─── @@ -101,19 +101,24 @@ static const PortDesc2 P2_SELECT_BANG_OUT[] = { {"false", "fires when false", PortKind2::BangNext}, }; -// new: va_args fields +// va_args templates +static const PortDesc2 P2_VA_FIELD = {"field", "constructor field"}; +static const PortDesc2 P2_VA_ARG = {"arg", "function argument"}; +static const PortDesc2 P2_VA_PARAM = {"param", "lambda parameter"}; + +// new: type as fixed input, va_args fields static const PortDesc2 P2_NEW_IN[] = { - {"field", "constructor field", PortKind2::Data, nullptr, true, true}, + {"type", "type to instantiate"}, }; -// call: va_args arguments +// call: function ref as fixed input, va_args arguments static const PortDesc2 P2_CALL_IN[] = { - {"arg", "function argument", PortKind2::Data, nullptr, true, true}, + {"fn", "function to call"}, }; -// call!: bang + va_args +// call!: bang + function ref, va_args arguments static const PortDesc2 P2_CALL_BANG_IN[] = { {"bang_in", "trigger", PortKind2::BangTrigger}, - {"arg", "function argument", PortKind2::Data, nullptr, true, true}, + {"fn", "function to call"}, }; // iterate: collection + fn(lambda) @@ -127,17 +132,15 @@ static const PortDesc2 P2_ITERATE_BANG_IN[] = { {"fn", "it=fn(it); while it!=end", PortKind2::Lambda}, }; -// lock: mutex + fn(lambda) + optional va_args params +// lock: mutex + fn(lambda), va_args handled by NodeType2::va_args static const PortDesc2 P2_LOCK_IN[] = { {"mutex", "mutex to lock"}, {"fn", "body under lock", PortKind2::Lambda}, - {"param", "lambda parameter", PortKind2::Data, nullptr, true, true}, }; static const PortDesc2 P2_LOCK_BANG_IN[] = { {"bang_in", "trigger", PortKind2::BangTrigger}, {"mutex", "mutex to lock"}, {"fn", "body under lock", PortKind2::Lambda}, - {"param", "lambda parameter", PortKind2::Data, nullptr, true, true}, }; // decl inputs @@ -221,9 +224,9 @@ static const NodeType2 NODE_TYPES2[] = { {NodeTypeID::Select, "select", "Select value by condition", P2_SELECT_IN, 3, P2_RESULT, 1, false, false}, - // new: va_args fields, 1 output + // new: type fixed input + va_args fields, 1 output {NodeTypeID::New, "new", "Instantiate a type", - P2_NEW_IN, 1, P2_RESULT, 1, false, false}, + P2_NEW_IN, 1, P2_RESULT, 1, false, false, &P2_VA_FIELD}, // dup: 1 input, 1 output {NodeTypeID::Dup, "dup", "Duplicate input to output", @@ -269,13 +272,13 @@ static const NodeType2 NODE_TYPES2[] = { {NodeTypeID::Ffi, "ffi", "Declare external function", P2_FFI_IN, 3, P2_NEXT, 1, false, true}, - // call: va_args, 1 output + // call: fn fixed input + va_args, 1 output {NodeTypeID::Call, "call", "Call function", - P2_CALL_IN, 1, P2_RESULT, 1, false, false}, + P2_CALL_IN, 1, P2_RESULT, 1, false, false, &P2_VA_ARG}, - // call!: bang + va_args, next + result + // call!: bang + fn fixed input + va_args, next + result {NodeTypeID::CallBang, "call!", "Call function (bang)", - P2_CALL_BANG_IN, 2, P2_NEXT_RESULT, 2, false, false}, + P2_CALL_BANG_IN, 2, P2_NEXT_RESULT, 2, false, false, &P2_VA_ARG}, // erase: 2 inputs, 1 output {NodeTypeID::Erase, "erase", "Erase from collection", @@ -337,13 +340,13 @@ static const NodeType2 NODE_TYPES2[] = { {NodeTypeID::Next, "next", "Advance iterator", P2_VALUE, 1, P2_RESULT, 1, false, false}, - // lock: mutex + fn + va_args, no outputs + // lock: mutex + fn fixed inputs + va_args params, no outputs {NodeTypeID::Lock, "lock", "Execute under mutex lock", - P2_LOCK_IN, 3, nullptr, 0, false, false}, + P2_LOCK_IN, 2, nullptr, 0, false, false, &P2_VA_PARAM}, - // lock!: bang + mutex + fn + va_args, next + // lock!: bang + mutex + fn fixed inputs + va_args params, next {NodeTypeID::LockBang, "lock!", "Execute under mutex lock (bang)", - P2_LOCK_BANG_IN, 4, P2_NEXT, 1, false, false}, + P2_LOCK_BANG_IN, 3, P2_NEXT, 1, false, false, &P2_VA_PARAM}, // resize!: bang + target + size, next {NodeTypeID::ResizeBang, "resize!", "Resize vector", diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index b9dfc5e..161e6b7 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -33,6 +33,60 @@ static inline ImVec2 v2add(ImVec2 a, ImVec2 b) { return {a.x + b.x, a.y + b.y}; static inline ImVec2 v2sub(ImVec2 a, ImVec2 b) { return {a.x - b.x, a.y - b.y}; } static inline ImVec2 v2mul(ImVec2 a, float s) { return {a.x * s, a.y * s}; } +// Computed node layout for drawing +struct NodeLayout { + ImVec2 pos; // top-left screen position + float width; + float height; + int num_in; + int num_out; + float zoom; + + ImVec2 input_pin_pos(int i) const { + return {pos.x + (i + 0.5f) * PIN_SPACING * zoom, pos.y}; + } + ImVec2 output_pin_pos(int i) const { + return {pos.x + (i + 0.5f) * PIN_SPACING * zoom, pos.y + height}; + } +}; + +static NodeLayout compute_node_layout(const FlowNodeBuilder& node, ImVec2 canvas_origin, float zoom) { + auto* nt = find_node_type2(node.type_id); + std::string display = nt ? nt->name : "?"; + std::string args = node.args_str(); + if (!args.empty()) display += " " + args; + + float font_size = ImGui::GetFontSize() * zoom; + ImVec2 text_sz = ImGui::CalcTextSize(display.c_str()); + float text_w = text_sz.x * zoom + 16.0f * zoom; + + auto count_net_args = [](const ParsedArgs2* pa) -> int { + if (!pa) return 0; + int n = 0; + for (auto& a : *pa) if (std::holds_alternative(a)) n++; + return n; + }; + int num_in = count_net_args(node.parsed_args.get()) + + count_net_args(node.parsed_va_args.get()) + + (int)node.remaps.size(); + int num_out = nt ? nt->num_outputs : 1; + if (node.type_id == NodeTypeID::Expr || node.type_id == NodeTypeID::ExprBang) { + int args_count = node.parsed_args ? (int)node.parsed_args->size() : 0; + if (node.type_id == NodeTypeID::ExprBang) num_out = 1 + std::max(1, args_count); + else num_out = std::max(1, args_count); + } + + float pin_w_top = std::max(0, num_in) * PIN_SPACING * zoom; + float pin_w_bot = std::max(0, num_out) * PIN_SPACING * zoom; + float node_w = std::max({NODE_MIN_WIDTH * zoom, text_w, pin_w_top, pin_w_bot}); + float node_h = NODE_HEIGHT * zoom; + + ImVec2 pos = {canvas_origin.x + node.position.x * zoom, + canvas_origin.y + node.position.y * zoom}; + + return {pos, node_w, node_h, num_in, num_out, zoom}; +} + static ImU32 pin_color(PortKind2 kind) { switch (kind) { case PortKind2::BangTrigger: @@ -113,8 +167,76 @@ void Editor2Pane::draw() { } } - // Draw nets (wires) - // TODO: draw connections between nodes via nets + // Draw wires by iterating each node's inputs/outputs/remaps + for (auto& [dst_id, dst_entry] : gb_->entries) { + if (!std::holds_alternative(*dst_entry)) continue; + auto& dst_node = std::get(*dst_entry); + auto* dst_nt = find_node_type2(dst_node.type_id); + if (!dst_nt) continue; + auto dst_layout = compute_node_layout(dst_node, canvas_origin, canvas_zoom_); + + // Helper: draw wire from a source net to destination pin at index dst_pin + auto draw_wire_to_pin = [&](int dst_pin, const BuilderEntryPtr& net_entry, const NodeId& net_id) { + if (!net_entry || !std::holds_alternative(*net_entry)) return; + auto& net = std::get(*net_entry); + if (net.is_the_unconnected) return; + + auto src_ptr = net.source.lock(); + if (!src_ptr || !std::holds_alternative(*src_ptr)) return; + auto& src_node = std::get(*src_ptr); + auto src_layout = compute_node_layout(src_node, canvas_origin, canvas_zoom_); + + // Find which output pin index on source this net comes from + // For now, use pin 0 as default — TODO: track output pin index per net + ImVec2 from = src_layout.output_pin_pos(0); + ImVec2 to = dst_layout.input_pin_pos(dst_pin); + + bool named = !net.auto_wire; + ImU32 col = named ? IM_COL32(200, 200, 100, 120) : COL_LINK; + float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); + float th = 2.5f * canvas_zoom_; + dl->AddBezierCubic(from, {from.x, from.y + dy}, {to.x, to.y - dy}, to, col, th); + + // Label for named nets + if (named) { + float font_size = ImGui::GetFontSize() * canvas_zoom_ * 0.8f; + if (font_size > 5.0f) { + ImVec2 mid = {(from.x + to.x) * 0.5f, (from.y + to.y) * 0.5f}; + ImVec2 text_sz = ImGui::CalcTextSize(net_id.c_str()); + float tw = text_sz.x * (font_size / ImGui::GetFontSize()); + float tth = text_sz.y * (font_size / ImGui::GetFontSize()); + float cx = mid.x - tw * 0.5f; + float cy = mid.y - tth * 0.5f; + dl->AddRectFilled({cx - 3, cy - 1}, {cx + tw + 3, cy + tth + 1}, + IM_COL32(30, 30, 40, 200), 3.0f); + dl->AddText(nullptr, font_size, {cx, cy}, IM_COL32(180, 220, 255, 255), net_id.c_str()); + } + } + }; + + // Helper: iterate ArgNet2 entries and draw wires, returns pin count used + auto draw_wires_from_args = [&](const ParsedArgs2* pa, int pin_start) -> int { + if (!pa) return 0; + int pin = pin_start; + for (auto& a : *pa) { + if (auto* an = std::get_if(&a)) { + draw_wire_to_pin(pin++, an->second, an->first); + } + } + return pin - pin_start; + }; + + // Base args → va_args → remaps + int pin = 0; + pin += draw_wires_from_args(dst_node.parsed_args.get(), pin); + pin += draw_wires_from_args(dst_node.parsed_va_args.get(), pin); + + // Remaps: $N pins (appended after base + va_args) + for (int i = 0; i < (int)dst_node.remaps.size(); i++) { + auto& remap = dst_node.remaps[i]; + draw_wire_to_pin(pin + i, remap.second, remap.first); + } + } dl->PopClipRect(); @@ -151,86 +273,86 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil auto* nt = find_node_type2(node.type_id); if (!nt) return; - // Compute display text + auto layout = compute_node_layout(node, canvas_origin, canvas_zoom_); + + // Display text std::string display = nt->name; std::string args = node.args_str(); if (!args.empty()) display += " " + args; - // Node rect - float font_size = ImGui::GetFontSize() * canvas_zoom_; - ImVec2 text_sz = ImGui::CalcTextSize(display.c_str()); - float text_w = text_sz.x * canvas_zoom_ + 16.0f * canvas_zoom_; - - // Pin counts - int num_in = nt->num_inputs + (node.parsed_args ? node.parsed_args->rewrite_input_count : 0); - int num_out = nt->num_outputs; - if (node.type_id == NodeTypeID::Expr || node.type_id == NodeTypeID::ExprBang) { - int args_count = node.parsed_args ? (int)node.parsed_args->size() : 0; - if (node.type_id == NodeTypeID::ExprBang) num_out = 1 + std::max(1, args_count); // next + outputs - else num_out = std::max(1, args_count); - } - - float pin_w_top = std::max(0, num_in) * PIN_SPACING * canvas_zoom_; - float pin_w_bot = std::max(0, num_out) * PIN_SPACING * canvas_zoom_; - float node_w = std::max({NODE_MIN_WIDTH * canvas_zoom_, text_w, pin_w_top, pin_w_bot}); - float node_h = NODE_HEIGHT * canvas_zoom_; - - ImVec2 pos = {canvas_origin.x + node.position.x * canvas_zoom_, - canvas_origin.y + node.position.y * canvas_zoom_}; - bool selected = selected_nodes_.count(id); bool has_error = !node.error.empty(); ImU32 col = has_error ? COL_NODE_ERR : (selected ? COL_NODE_SEL : COL_NODE); - dl->AddRectFilled(pos, {pos.x + node_w, pos.y + node_h}, col, 4.0f * canvas_zoom_); - dl->AddRect(pos, {pos.x + node_w, pos.y + node_h}, IM_COL32(80, 80, 100, 255), 4.0f * canvas_zoom_); + dl->AddRectFilled(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, + col, 4.0f * canvas_zoom_); + dl->AddRect(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, + IM_COL32(80, 80, 100, 255), 4.0f * canvas_zoom_); // Text + float font_size = ImGui::GetFontSize() * canvas_zoom_; if (font_size > 5.0f) { + ImVec2 text_sz = ImGui::CalcTextSize(display.c_str()); float tw = text_sz.x * canvas_zoom_; - float cx = pos.x + (node_w - tw) * 0.5f; - float cy = pos.y + (node_h - font_size) * 0.5f; + float cx = layout.pos.x + (layout.width - tw) * 0.5f; + float cy = layout.pos.y + (layout.height - font_size) * 0.5f; dl->AddText(nullptr, font_size, {cx, cy}, COL_TEXT, display.c_str()); } // Draw input pins (top) + // Pin order: parsed_args ArgNet2, then va_args ArgNet2, then remaps float pr = PIN_RADIUS * canvas_zoom_; - for (int i = 0; i < num_in; i++) { - float px = pos.x + (i + 0.5f) * PIN_SPACING * canvas_zoom_; - float py = pos.y; - + auto count_net_args_in = [](const ParsedArgs2* pa) -> int { + if (!pa) return 0; + int n = 0; + for (auto& a : *pa) if (std::holds_alternative(a)) n++; + return n; + }; + int base_pin_count = count_net_args_in(node.parsed_args.get()); + int va_pin_count = count_net_args_in(node.parsed_va_args.get()); + for (int i = 0; i < layout.num_in; i++) { + ImVec2 pp = layout.input_pin_pos(i); PortKind2 kind = PortKind2::Data; - if (nt->input_ports && i < nt->num_inputs) kind = nt->input_ports[i].kind; + if (i < base_pin_count) { + // Map visible pin index to descriptor port (skip bang triggers since they're not in parsed_args) + if (nt->input_ports) { + int vis = 0; + for (int p = 0; p < nt->num_inputs; p++) { + if (nt->input_ports[p].kind != PortKind2::BangTrigger) { + if (vis == i) { kind = nt->input_ports[p].kind; break; } + vis++; + } + } + } + } else if (i < base_pin_count + va_pin_count) { + kind = nt->va_args ? nt->va_args->kind : PortKind2::Data; + } ImU32 pc = pin_color(kind); if (kind == PortKind2::BangTrigger) { - dl->AddRectFilled({px - pr, py - pr}, {px + pr, py + pr}, pc); + dl->AddRectFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y + pr}, pc); } else if (kind == PortKind2::Lambda) { - // Triangle for lambda - dl->AddTriangleFilled({px - pr, py - pr}, {px + pr, py}, {px - pr, py + pr}, pc); + dl->AddTriangleFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x - pr, pp.y + pr}, pc); } else { - dl->AddCircleFilled({px, py}, pr, pc); + dl->AddCircleFilled(pp, pr, pc); } } // Draw output pins (bottom) - for (int i = 0; i < num_out; i++) { - float px = pos.x + (i + 0.5f) * PIN_SPACING * canvas_zoom_; - float py = pos.y + node_h; - + for (int i = 0; i < layout.num_out; i++) { + ImVec2 pp = layout.output_pin_pos(i); PortKind2 kind = PortKind2::Data; if (nt->output_ports && i < nt->num_outputs) kind = nt->output_ports[i].kind; ImU32 pc = pin_color(kind); if (kind == PortKind2::BangNext) { - dl->AddRectFilled({px - pr, py - pr}, {px + pr, py + pr}, pc); + dl->AddRectFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y + pr}, pc); } else { - dl->AddCircleFilled({px, py}, pr, pc); + dl->AddCircleFilled(pp, pr, pc); } } } -void Editor2Pane::draw_net(ImDrawList* dl, const NodeId& id, const NetBuilder& net, - ImVec2 canvas_origin) { - // TODO: draw wires between connected nodes +void Editor2Pane::draw_net(ImDrawList*, const NodeId&, const NetBuilder&, ImVec2) { + // Unused — wires drawn per-node in draw() } From fb3d96da7ed8ded12e7d5959e8d5b5c316ad8d55 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 02:47:26 +0200 Subject: [PATCH 17/86] not perfect, but progress is progress --- src/atto/graphbuilder.cpp | 171 +++++++++++++++++++++++++++++++++++--- src/attoflow/editor2.cpp | 59 ++++++++++--- 2 files changed, 206 insertions(+), 24 deletions(-) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index 616afbc..e5d0bd3 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -1,5 +1,6 @@ #include "graphbuilder.h" #include "node_types2.h" +#include "expr.h" #include #include #include @@ -381,11 +382,163 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { auto [_, net_ptr] = gb->find_or_create_net(net_name, true); std::get(*net_ptr).source = node_entry; } - // Wire nets from inputs (this node is destination) - for (auto& net_name : cur_inputs) { - if (net_name.empty()) continue; - auto [_, net_ptr] = gb->find_or_create_net(net_name); - std::get(*net_ptr).destinations.push_back(node_entry); + + // ─── v0 → v1 port mapping: merge inputs + args by port name ─── + if (!cur_inputs.empty() && !cur_shadow) { + auto* old_nt = find_node_type(cur_type.c_str()); + auto* new_nt = find_node_type2(nb.type_id); + bool is_expr = is_any_of(nb.type_id, NodeTypeID::Expr, NodeTypeID::ExprBang); + bool args_are_type = is_any_of(nb.type_id, NodeTypeID::Cast, NodeTypeID::New); + + // Helper: resolve net name to ArgNet2 and register destination + auto resolve_net = [&](const std::string& net_name) -> ArgNet2 { + auto [resolved, ptr] = gb->find_or_create_net( + net_name.empty() ? "$unconnected" : net_name); + if (!net_name.empty()) + std::get(*ptr).destinations.push_back(node_entry); + return {resolved, ptr}; + }; + + if (is_expr || !old_nt || !new_nt) { + // Expr nodes or unknown types: simple positional mapping + // inputs map directly as ArgNet2 entries prepended to parsed_args + auto merged = std::make_shared(); + for (auto& net_name : cur_inputs) + merged->push_back(resolve_net(net_name)); + if (nb.parsed_args) { + for (auto& a : *nb.parsed_args) + merged->push_back(std::move(a)); + merged->rewrite_input_count = nb.parsed_args->rewrite_input_count; + } + nb.parsed_args = std::move(merged); + } else { + // Name-based mapping using old and new descriptors + + // Step 1: Build old pin name list (matching inputs array order) + std::vector old_pin_names; + // Triggers first + for (int i = 0; i < old_nt->num_triggers; i++) { + if (old_nt->trigger_ports) + old_pin_names.push_back(old_nt->trigger_ports[i].name); + else + old_pin_names.push_back("bang_in"); + } + // Data pins depend on args + std::string args_joined; + for (auto& a : cur_args) { + if (!args_joined.empty()) args_joined += " "; + args_joined += a; + } + + if (args_are_type) { + // Type nodes: all descriptor inputs become pins + for (int i = 0; i < old_nt->inputs; i++) { + if (old_nt->input_ports && i < old_nt->inputs) + old_pin_names.push_back(old_nt->input_ports[i].name); + else + old_pin_names.push_back(std::to_string(i)); + } + } else { + auto info = compute_inline_args(args_joined, old_nt->inputs); + // $N ref pins first + int ref_pins = (info.pin_slots.max_slot >= 0) ? (info.pin_slots.max_slot + 1) : 0; + for (int i = 0; i < ref_pins; i++) { + bool is_lambda = info.pin_slots.is_lambda_slot(i); + old_pin_names.push_back(is_lambda ? ("@" + std::to_string(i)) : std::to_string(i)); + } + // Remaining descriptor pins + for (int i = info.num_inline_args; i < old_nt->inputs; i++) { + if (old_nt->input_ports && i < old_nt->inputs) + old_pin_names.push_back(old_nt->input_ports[i].name); + else + old_pin_names.push_back(std::to_string(i)); + } + } + + // Step 2: Build port_name → ArgNet2 map from inputs array + std::map net_map; + for (int i = 0; i < (int)cur_inputs.size() && i < (int)old_pin_names.size(); i++) { + net_map[old_pin_names[i]] = resolve_net(cur_inputs[i]); + } + + // Step 3: Build port_name → parsed_value map from inlined args + // Inlined args cover input_ports[0..num_inline_args-1] + std::map inline_map; + if (!args_are_type && nb.parsed_args) { + auto info = compute_inline_args(args_joined, old_nt->inputs); + int num_inline = std::min(info.num_inline_args, old_nt->inputs); + for (int i = 0; i < num_inline && i < (int)nb.parsed_args->size(); i++) { + if (old_nt->input_ports && i < old_nt->inputs) + inline_map[old_nt->input_ports[i].name] = std::move((*nb.parsed_args)[i]); + } + } + + // Step 4: Build unified parsed_args in new descriptor order + auto merged = std::make_shared(); + if (nb.parsed_args) + merged->rewrite_input_count = nb.parsed_args->rewrite_input_count; + + // Helper: find value by port name with fallback for bang→bang_in rename + auto find_by_name = [&](const char* name) -> std::pair { + auto net_it = net_map.find(name); + if (net_it != net_map.end()) + return {true, std::move(net_it->second)}; + auto inline_it = inline_map.find(name); + if (inline_it != inline_map.end()) + return {true, std::move(inline_it->second)}; + // Fallback: old "bang" → new "bang_in" + if (strcmp(name, "bang_in") == 0) { + auto it2 = net_map.find("bang"); + if (it2 != net_map.end()) + return {true, std::move(it2->second)}; + } + return {false, {}}; + }; + + // Pass 1: fill by name matching + std::vector filled(new_nt->num_inputs, false); + for (int i = 0; i < new_nt->num_inputs; i++) { + auto [found, value] = find_by_name(new_nt->input_ports[i].name); + if (found) { + merged->push_back(std::move(value)); + filled[i] = true; + } else { + merged->push_back(resolve_net("")); // placeholder + } + } + + // Pass 2: fill unfilled non-bang slots from unconsumed parsed_args + // inline_map consumed parsed_args[0..num_inline-1]; rest are available + if (nb.parsed_args) { + int consumed = 0; + if (!args_are_type) { + auto info2 = compute_inline_args(args_joined, old_nt->inputs); + consumed = std::min(info2.num_inline_args, (int)nb.parsed_args->size()); + consumed = std::min(consumed, old_nt->inputs); + } + int arg_cursor = consumed; + for (int i = 0; i < new_nt->num_inputs; i++) { + if (!filled[i] && new_nt->input_ports[i].kind != PortKind2::BangTrigger) { + if (arg_cursor < (int)nb.parsed_args->size()) { + (*merged)[i] = std::move((*nb.parsed_args)[arg_cursor++]); + filled[i] = true; + } + } + } + // Remaining args beyond descriptor slots → appended (for va_args split later) + for (; arg_cursor < (int)nb.parsed_args->size(); arg_cursor++) + merged->push_back(std::move((*nb.parsed_args)[arg_cursor])); + } + + nb.parsed_args = std::move(merged); + } + } else if (!cur_inputs.empty() && cur_shadow) { + // Shadows: inputs wired as net destinations (handled during folding) + for (auto& net_name : cur_inputs) { + if (net_name.empty()) continue; + auto [_, net_ptr] = gb->find_or_create_net(net_name); + std::get(*net_ptr).destinations.push_back(node_entry); + } } cur_id.clear(); cur_type.clear(); cur_args.clear(); @@ -519,12 +672,8 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { auto* nt = find_node_type2(node.type_id); if (!nt || !nt->va_args || !node.parsed_args) continue; - // Count non-bang fixed inputs (args don't include bang triggers) - int fixed_args = 0; - for (int i = 0; i < nt->num_inputs; i++) { - if (nt->input_ports && nt->input_ports[i].kind != PortKind2::BangTrigger) - fixed_args++; - } + // Split at descriptor input count — inputs are merged first, then args + int fixed_args = nt->num_inputs; if ((int)node.parsed_args->size() > fixed_args) { node.parsed_va_args = std::make_shared(); diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 161e6b7..32252a9 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -300,7 +300,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil } // Draw input pins (top) - // Pin order: parsed_args ArgNet2, then va_args ArgNet2, then remaps + // Pin order: parsed_args ArgNet2 (inputs merged first), then va_args ArgNet2, then remaps float pr = PIN_RADIUS * canvas_zoom_; auto count_net_args_in = [](const ParsedArgs2* pa) -> int { if (!pa) return 0; @@ -313,18 +313,9 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil for (int i = 0; i < layout.num_in; i++) { ImVec2 pp = layout.input_pin_pos(i); PortKind2 kind = PortKind2::Data; - if (i < base_pin_count) { - // Map visible pin index to descriptor port (skip bang triggers since they're not in parsed_args) - if (nt->input_ports) { - int vis = 0; - for (int p = 0; p < nt->num_inputs; p++) { - if (nt->input_ports[p].kind != PortKind2::BangTrigger) { - if (vis == i) { kind = nt->input_ports[p].kind; break; } - vis++; - } - } - } - } else if (i < base_pin_count + va_pin_count) { + if (i < base_pin_count && nt->input_ports && i < nt->num_inputs) { + kind = nt->input_ports[i].kind; + } else if (i >= base_pin_count && i < base_pin_count + va_pin_count) { kind = nt->va_args ? nt->va_args->kind : PortKind2::Data; } @@ -351,6 +342,48 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil dl->AddCircleFilled(pp, pr, pc); } } + + // Hover tooltip: show parsed_args, parsed_va_args, remaps + ImVec2 mouse = ImGui::GetMousePos(); + if (mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && + mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height) { + ImGui::BeginTooltip(); + ImGui::Text("id: %s", id.c_str()); + if (node.parsed_args) { + ImGui::Text("parsed_args (%d):", (int)node.parsed_args->size()); + for (int i = 0; i < (int)node.parsed_args->size(); i++) { + auto& a = (*node.parsed_args)[i]; + if (auto* n = std::get_if(&a)) + ImGui::Text(" [%d] net: %s", i, n->first.c_str()); + else if (auto* e = std::get_if(&a)) + ImGui::Text(" [%d] expr: %s", i, e->expr.c_str()); + else if (auto* s = std::get_if(&a)) + ImGui::Text(" [%d] str: %s", i, s->value.c_str()); + else if (auto* v = std::get_if(&a)) + ImGui::Text(" [%d] num: %g", i, v->value); + } + } + if (node.parsed_va_args && !node.parsed_va_args->empty()) { + ImGui::Text("parsed_va_args (%d):", (int)node.parsed_va_args->size()); + for (int i = 0; i < (int)node.parsed_va_args->size(); i++) { + auto& a = (*node.parsed_va_args)[i]; + if (auto* n = std::get_if(&a)) + ImGui::Text(" [%d] net: %s", i, n->first.c_str()); + else if (auto* e = std::get_if(&a)) + ImGui::Text(" [%d] expr: %s", i, e->expr.c_str()); + else if (auto* s = std::get_if(&a)) + ImGui::Text(" [%d] str: %s", i, s->value.c_str()); + else if (auto* v = std::get_if(&a)) + ImGui::Text(" [%d] num: %g", i, v->value); + } + } + if (!node.remaps.empty()) { + ImGui::Text("remaps (%d):", (int)node.remaps.size()); + for (int i = 0; i < (int)node.remaps.size(); i++) + ImGui::Text(" $%d -> %s", i, node.remaps[i].first.c_str()); + } + ImGui::EndTooltip(); + } } void Editor2Pane::draw_net(ImDrawList*, const NodeId&, const NetBuilder&, ImVec2) { From 4120050a5e0859bf03fcf146f10dc5bed45cc63a Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 02:57:07 +0200 Subject: [PATCH 18/86] towards lambda link rendering --- src/atto/graphbuilder.cpp | 60 ++++++++++++++++++++++++++++++--------- src/atto/graphbuilder.h | 1 + src/attoflow/editor2.cpp | 37 +++++++++++++++--------- 3 files changed, 71 insertions(+), 27 deletions(-) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index e5d0bd3..64e6576 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -221,11 +221,15 @@ void GraphBuilder::ensure_unconnected() { } std::pair GraphBuilder::find_or_create_net(const NodeId& name, bool for_source) { - auto [id, ptr] = find_net(name); // throws if name exists as a node - if (ptr) { - if (for_source && !std::get(*ptr).source.expired()) - throw std::logic_error("find_or_create_net(\"" + name + "\"): net already has a source"); - return {id, ptr}; + auto it = entries.find(name); + if (it != entries.end()) { + if (std::holds_alternative(*it->second)) { + if (for_source && !std::get(*it->second).source.expired()) + throw std::logic_error("find_or_create_net(\"" + name + "\"): net already has a source"); + return {it->first, it->second}; + } + // Exists as a node — don't overwrite + return {it->first, nullptr}; } auto entry = std::make_shared(NetBuilder{}); auto& net = std::get(*entry); @@ -251,7 +255,13 @@ std::pair GraphBuilder::find_net(const NodeId& name) { auto it = entries.find(name); if (it == entries.end()) return {name, nullptr}; if (!std::holds_alternative(*it->second)) - throw std::logic_error("find_net(\"" + name + "\"): entry exists but is a FlowNodeBuilder, not a NetBuilder"); + return {name, nullptr}; // exists as node, not net + return {it->first, it->second}; +} + +std::pair GraphBuilder::find_entity(const NodeId& id) { + auto it = entries.find(id); + if (it == entries.end()) return {id, nullptr}; return {it->first, it->second}; } @@ -377,10 +387,14 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { auto node_entry = gb->find(cur_id); // Wire nets from outputs (this node is source) + // Skip -as_lambda entries — in v1, lambda captures reference nodes directly for (auto& net_name : cur_outputs) { if (net_name.empty()) continue; + if (net_name.size() > 10 && net_name.compare(net_name.size() - 10, 10, "-as_lambda") == 0) + continue; auto [_, net_ptr] = gb->find_or_create_net(net_name, true); - std::get(*net_ptr).source = node_entry; + if (net_ptr && std::holds_alternative(*net_ptr)) + std::get(*net_ptr).source = node_entry; } // ─── v0 → v1 port mapping: merge inputs + args by port name ─── @@ -390,13 +404,30 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { bool is_expr = is_any_of(nb.type_id, NodeTypeID::Expr, NodeTypeID::ExprBang); bool args_are_type = is_any_of(nb.type_id, NodeTypeID::Cast, NodeTypeID::New); - // Helper: resolve net name to ArgNet2 and register destination + // Helper: resolve net/node name to ArgNet2 and register destination auto resolve_net = [&](const std::string& net_name) -> ArgNet2 { - auto [resolved, ptr] = gb->find_or_create_net( - net_name.empty() ? "$unconnected" : net_name); - if (!net_name.empty()) - std::get(*ptr).destinations.push_back(node_entry); - return {resolved, ptr}; + if (net_name.empty()) { + auto [resolved, ptr] = gb->find_or_create_net("$unconnected"); + return {resolved, ptr}; + } + // Strip -as_lambda suffix → resolve to node entry directly + std::string resolved_name = net_name; + if (resolved_name.size() > 10 && + resolved_name.compare(resolved_name.size() - 10, 10, "-as_lambda") == 0) { + resolved_name.resize(resolved_name.size() - 10); + } + // Try finding as any entry (node or net) + auto ptr = gb->find(resolved_name); + if (ptr) { + // If it's a net, register as destination + if (std::holds_alternative(*ptr)) + std::get(*ptr).destinations.push_back(node_entry); + return {resolved_name, ptr}; + } + // Not found yet — create as net + auto [id, net_ptr] = gb->find_or_create_net(resolved_name); + std::get(*net_ptr).destinations.push_back(node_entry); + return {id, net_ptr}; }; if (is_expr || !old_nt || !new_nt) { @@ -537,7 +568,8 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { for (auto& net_name : cur_inputs) { if (net_name.empty()) continue; auto [_, net_ptr] = gb->find_or_create_net(net_name); - std::get(*net_ptr).destinations.push_back(node_entry); + if (net_ptr && std::holds_alternative(*net_ptr)) + std::get(*net_ptr).destinations.push_back(node_entry); } } diff --git a/src/atto/graphbuilder.h b/src/atto/graphbuilder.h index 4e3bd7c..8ab7b8b 100644 --- a/src/atto/graphbuilder.h +++ b/src/atto/graphbuilder.h @@ -96,6 +96,7 @@ struct GraphBuilder { std::pair find_node(const NodeId& id); std::pair find_net(const NodeId& name); + std::pair find_entity(const NodeId& id); // returns node or net void compact(); }; diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 32252a9..1e20402 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -175,23 +175,34 @@ void Editor2Pane::draw() { if (!dst_nt) continue; auto dst_layout = compute_node_layout(dst_node, canvas_origin, canvas_zoom_); - // Helper: draw wire from a source net to destination pin at index dst_pin - auto draw_wire_to_pin = [&](int dst_pin, const BuilderEntryPtr& net_entry, const NodeId& net_id) { - if (!net_entry || !std::holds_alternative(*net_entry)) return; - auto& net = std::get(*net_entry); - if (net.is_the_unconnected) return; - - auto src_ptr = net.source.lock(); - if (!src_ptr || !std::holds_alternative(*src_ptr)) return; - auto& src_node = std::get(*src_ptr); - auto src_layout = compute_node_layout(src_node, canvas_origin, canvas_zoom_); - - // Find which output pin index on source this net comes from + // Helper: draw wire from a source to destination pin at index dst_pin + // Source can be a NetBuilder (regular wire) or FlowNodeBuilder (lambda capture) + auto draw_wire_to_pin = [&](int dst_pin, const BuilderEntryPtr& entry, const NodeId& net_id) { + if (!entry) return; + + FlowNodeBuilder* src_node_ptr = nullptr; + bool named = false; + + if (std::holds_alternative(*entry)) { + auto& net = std::get(*entry); + if (net.is_the_unconnected) return; + auto src_ptr = net.source.lock(); + if (!src_ptr || !std::holds_alternative(*src_ptr)) return; + src_node_ptr = &std::get(*src_ptr); + named = !net.auto_wire; + } else if (std::holds_alternative(*entry)) { + // Lambda capture: entry IS the source node + src_node_ptr = &std::get(*entry); + } else { + return; + } + + auto src_layout = compute_node_layout(*src_node_ptr, canvas_origin, canvas_zoom_); + // For now, use pin 0 as default — TODO: track output pin index per net ImVec2 from = src_layout.output_pin_pos(0); ImVec2 to = dst_layout.input_pin_pos(dst_pin); - bool named = !net.auto_wire; ImU32 col = named ? IM_COL32(200, 200, 100, 120) : COL_LINK; float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); float th = 2.5f * canvas_zoom_; From 3d664994ad58ebdfa73abec9618fdde672a61a25 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 03:05:16 +0200 Subject: [PATCH 19/86] lambda grab rendering --- src/atto/graphbuilder.cpp | 23 +++++++++++++++++++++ src/attoflow/editor2.cpp | 42 +++++++++++++++++++++++++++++++-------- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index 64e6576..01f2e1c 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -613,6 +613,29 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { } flush_node(); + // ─── Re-resolve ArgNet2 entries pointing to stale placeholders ─── + // When a lambda capture references a node parsed later in the file, + // resolve_net creates a NetBuilder placeholder. Now re-resolve to the actual node. + { + auto fixup_args = [&](ParsedArgs2* pa) { + if (!pa) return; + for (auto& a : *pa) { + if (auto* an = std::get_if(&a)) { + if (!an->second || !std::holds_alternative(*an->second)) continue; + auto actual = gb->find(an->first); + if (actual && std::holds_alternative(*actual)) + an->second = actual; + } + } + }; + for (auto& [id, entry] : gb->entries) { + if (!std::holds_alternative(*entry)) continue; + auto& node = std::get(*entry); + fixup_args(node.parsed_args.get()); + fixup_args(node.parsed_va_args.get()); + } + } + // ─── Fold shadow nodes into parents ─── auto unconnected_entry = gb->find("$unconnected"); diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 1e20402..1c0267a 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -48,6 +48,9 @@ struct NodeLayout { ImVec2 output_pin_pos(int i) const { return {pos.x + (i + 0.5f) * PIN_SPACING * zoom, pos.y + height}; } + ImVec2 lambda_grab_pos() const { + return {pos.x, pos.y + height * 0.5f}; + } }; static NodeLayout compute_node_layout(const FlowNodeBuilder& node, ImVec2 canvas_origin, float zoom) { @@ -182,6 +185,7 @@ void Editor2Pane::draw() { FlowNodeBuilder* src_node_ptr = nullptr; bool named = false; + bool is_lambda = false; if (std::holds_alternative(*entry)) { auto& net = std::get(*entry); @@ -193,20 +197,30 @@ void Editor2Pane::draw() { } else if (std::holds_alternative(*entry)) { // Lambda capture: entry IS the source node src_node_ptr = &std::get(*entry); + is_lambda = true; } else { return; } auto src_layout = compute_node_layout(*src_node_ptr, canvas_origin, canvas_zoom_); - - // For now, use pin 0 as default — TODO: track output pin index per net - ImVec2 from = src_layout.output_pin_pos(0); ImVec2 to = dst_layout.input_pin_pos(dst_pin); - - ImU32 col = named ? IM_COL32(200, 200, 100, 120) : COL_LINK; - float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); float th = 2.5f * canvas_zoom_; - dl->AddBezierCubic(from, {from.x, from.y + dy}, {to.x, to.y - dy}, to, col, th); + + ImVec2 from; + if (is_lambda) { + // Lambda wire: from grab (middle-left) with horizontal-then-vertical curve + from = src_layout.lambda_grab_pos(); + float dx = std::max(std::abs(to.x - from.x) * 0.5f, 30.0f * canvas_zoom_); + float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); + ImU32 col = IM_COL32(180, 130, 255, 200); + dl->AddBezierCubic(from, {from.x - dx, from.y}, {to.x, to.y - dy}, to, col, th); + } else { + // Regular wire: from output pin (bottom) with vertical curve + from = src_layout.output_pin_pos(0); // TODO: track output pin index + ImU32 col = named ? IM_COL32(200, 200, 100, 120) : COL_LINK; + float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); + dl->AddBezierCubic(from, {from.x, from.y + dy}, {to.x, to.y - dy}, to, col, th); + } // Label for named nets if (named) { @@ -334,7 +348,8 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil if (kind == PortKind2::BangTrigger) { dl->AddRectFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y + pr}, pc); } else if (kind == PortKind2::Lambda) { - dl->AddTriangleFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x - pr, pp.y + pr}, pc); + // Down-pointing triangle for lambda inputs (matching editor1) + dl->AddTriangleFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y - pr}, {pp.x, pp.y + pr}, pc); } else { dl->AddCircleFilled(pp, pr, pc); } @@ -354,6 +369,17 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil } } + // Lambda grab handle (left-pointing triangle, middle-left) + { + ImVec2 gp = layout.lambda_grab_pos(); + ImU32 lc = IM_COL32(180, 130, 255, 255); + dl->AddTriangleFilled( + {gp.x + pr, gp.y - pr}, + {gp.x - pr, gp.y}, + {gp.x + pr, gp.y + pr}, + lc); + } + // Hover tooltip: show parsed_args, parsed_va_args, remaps ImVec2 mouse = ImGui::GetMousePos(); if (mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && From 25cd37358a8c5f5e3566a5a34f49beb83700c196 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 03:13:19 +0200 Subject: [PATCH 20/86] Cleanup node_types2.h --- src/atto/node_types2.h | 111 ++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/src/atto/node_types2.h b/src/atto/node_types2.h index 94951df..9bda1e0 100644 --- a/src/atto/node_types2.h +++ b/src/atto/node_types2.h @@ -18,6 +18,13 @@ struct PortDesc2 { bool optional = false; }; +enum class NodeKind2 : uint8_t { + Flow, // dataflow node — side-bang (right-middle) + Banged, // bang trigger input (top) + bang next output (bottom) + Event, // event source — bang next output (bottom), no bang input + Declaration, // compile-time — bang trigger input (top) + bang next output (bottom) +}; + struct NodeType2 { NodeTypeID type_id; const char* name; @@ -26,9 +33,13 @@ struct NodeType2 { int num_inputs; const PortDesc2* output_ports; int num_outputs; - bool is_event = false; - bool is_declaration = false; + NodeKind2 kind = NodeKind2::Flow; const PortDesc2* va_args = nullptr; // nullptr = no va_args, else template for repeating pins + + bool is_banged() const { return kind == NodeKind2::Banged || kind == NodeKind2::Event || kind == NodeKind2::Declaration; } + bool is_declaration() const { return kind == NodeKind2::Declaration; } + bool is_flow() const { return kind == NodeKind2::Flow; } + bool is_event() const { return kind == NodeKind2::Event; } }; // ─── Port descriptor arrays ─── @@ -197,18 +208,6 @@ static const PortDesc2 P2_RESIZE_IN[] = { {"size", "new size", PortKind2::Data, "s32"}, }; -// on_key outputs: next + data -static const PortDesc2 P2_KEY_OUT[] = { - {"next", "fires on key event", PortKind2::BangNext}, - {"midi_key", "MIDI note number", PortKind2::Data, "u8"}, - {"freq", "frequency in Hz", PortKind2::Data, "f32"}, -}; -// on_key_up outputs: next + midi_key only -static const PortDesc2 P2_KEY_UP_OUT[] = { - {"next", "fires on key release", PortKind2::BangNext}, - {"midi_key", "MIDI note number", PortKind2::Data, "u8"}, - {"freq", "frequency in Hz", PortKind2::Data, "f32"}, -}; // event! outputs static const PortDesc2 P2_EVENT_OUT[] = {{"next", "fires on event", PortKind2::BangNext}}; @@ -218,155 +217,155 @@ static const PortDesc2 P2_EVENT_OUT[] = {{"next", "fires on event", PortKind2::B static const NodeType2 NODE_TYPES2[] = { // expr: no fixed inputs, outputs = args count {NodeTypeID::Expr, "expr", "Evaluate expression", - nullptr, 0, P2_RESULT, 1, false, false}, + nullptr, 0, P2_RESULT, 1}, // select: 3 fixed inputs, 1 output {NodeTypeID::Select, "select", "Select value by condition", - P2_SELECT_IN, 3, P2_RESULT, 1, false, false}, + P2_SELECT_IN, 3, P2_RESULT, 1}, // new: type fixed input + va_args fields, 1 output {NodeTypeID::New, "new", "Instantiate a type", - P2_NEW_IN, 1, P2_RESULT, 1, false, false, &P2_VA_FIELD}, + P2_NEW_IN, 1, P2_RESULT, 1, NodeKind2::Flow, &P2_VA_FIELD}, // dup: 1 input, 1 output {NodeTypeID::Dup, "dup", "Duplicate input to output", - P2_VALUE, 1, P2_RESULT, 1, false, false}, + P2_VALUE, 1, P2_RESULT, 1}, // str: 1 input, 1 output {NodeTypeID::Str, "str", "Convert to string", - P2_VALUE, 1, P2_RESULT, 1, false, false}, + P2_VALUE, 1, P2_RESULT, 1}, // void: no inputs, 1 output {NodeTypeID::Void, "void", "Void result", - nullptr, 0, P2_RESULT, 1, false, false}, + nullptr, 0, P2_RESULT, 1}, // discard!: bang + value, next output {NodeTypeID::DiscardBang, "discard!", "Discard value, pass bang", - P2_DISCARD_BANG_IN, 2, P2_NEXT, 1, false, false}, + P2_DISCARD_BANG_IN, 2, P2_NEXT, 1, NodeKind2::Banged}, // discard: 1 input, no outputs {NodeTypeID::Discard, "discard", "Discard input values", - P2_VALUE, 1, nullptr, 0, false, false}, + P2_VALUE, 1, nullptr, 0}, // decl_type {NodeTypeID::DeclType, "decl_type", "Declare a type", - P2_DECL_TYPE_IN, 3, P2_DECL_TYPE_OUT, 2, false, true}, + P2_DECL_TYPE_IN, 3, P2_DECL_TYPE_OUT, 2, NodeKind2::Declaration}, // decl_var {NodeTypeID::DeclVar, "decl_var", "Declare a variable", - P2_DECL_VAR_IN, 3, P2_DECL_VAR_OUT, 2, false, true}, + P2_DECL_VAR_IN, 3, P2_DECL_VAR_OUT, 2, NodeKind2::Declaration}, // decl {NodeTypeID::Decl, "decl", "Compile-time entry point", - nullptr, 0, P2_DECL_OUT, 1, false, true}, + nullptr, 0, P2_DECL_OUT, 1, NodeKind2::Declaration}, // decl_event {NodeTypeID::DeclEvent, "decl_event", "Declare event", - P2_DECL_EVENT_IN, 3, P2_NEXT, 1, false, true}, + P2_DECL_EVENT_IN, 3, P2_NEXT, 1, NodeKind2::Declaration}, // decl_import {NodeTypeID::DeclImport, "decl_import","Import module", - P2_DECL_IMPORT_IN, 2, P2_NEXT, 1, false, true}, + P2_DECL_IMPORT_IN, 2, P2_NEXT, 1, NodeKind2::Declaration}, // ffi {NodeTypeID::Ffi, "ffi", "Declare external function", - P2_FFI_IN, 3, P2_NEXT, 1, false, true}, + P2_FFI_IN, 3, P2_NEXT, 1, NodeKind2::Declaration}, // call: fn fixed input + va_args, 1 output {NodeTypeID::Call, "call", "Call function", - P2_CALL_IN, 1, P2_RESULT, 1, false, false, &P2_VA_ARG}, + P2_CALL_IN, 1, P2_RESULT, 1, NodeKind2::Flow, &P2_VA_ARG}, // call!: bang + fn fixed input + va_args, next + result {NodeTypeID::CallBang, "call!", "Call function (bang)", - P2_CALL_BANG_IN, 2, P2_NEXT_RESULT, 2, false, false, &P2_VA_ARG}, + P2_CALL_BANG_IN, 2, P2_NEXT_RESULT, 2, NodeKind2::Banged, &P2_VA_ARG}, // erase: 2 inputs, 1 output {NodeTypeID::Erase, "erase", "Erase from collection", - P2_ERASE_IN, 2, P2_RESULT, 1, false, false}, + P2_ERASE_IN, 2, P2_RESULT, 1}, // output_mix! {NodeTypeID::OutputMixBang, "output_mix!","Mix into audio output", - P2_OUTPUT_MIX_IN, 2, nullptr, 0, false, false}, + P2_OUTPUT_MIX_IN, 2, nullptr, 0, NodeKind2::Banged}, // append: 2 inputs, 1 output {NodeTypeID::Append, "append", "Append to collection", - P2_APPEND_IN, 2, P2_RESULT, 1, false, false}, + P2_APPEND_IN, 2, P2_RESULT, 1}, // append!: bang + 2 inputs, next + result {NodeTypeID::AppendBang, "append!", "Append to collection (bang)", - P2_APPEND_BANG_IN, 3, P2_NEXT_RESULT, 2, false, false}, + P2_APPEND_BANG_IN, 3, P2_NEXT_RESULT, 2, NodeKind2::Banged}, // store: 2 inputs, no outputs {NodeTypeID::Store, "store", "Store value", - P2_STORE_IN, 2, nullptr, 0, false, false}, + P2_STORE_IN, 2, nullptr, 0}, // store!: bang + 2 inputs, next {NodeTypeID::StoreBang, "store!", "Store value (bang)", - P2_STORE_BANG_IN, 3, P2_NEXT, 1, false, false}, + P2_STORE_BANG_IN, 3, P2_NEXT, 1, NodeKind2::Banged}, // event!: no inputs, next output {NodeTypeID::EventBang, "event!", "Event source", - nullptr, 0, P2_EVENT_OUT, 1, true, false}, + nullptr, 0, P2_EVENT_OUT, 1, NodeKind2::Event}, - // on_key_down!: no inputs, next + 2 data outputs - {NodeTypeID::OnKeyDownBang, "on_key_down!","Key press event", - nullptr, 0, P2_KEY_OUT, 3, true, false}, + // on_key_down! — removed + {NodeTypeID::OnKeyDownBang, "on_key_down!","(removed)", + nullptr, 0, nullptr, 0}, - // on_key_up!: no inputs, next + 2 data outputs - {NodeTypeID::OnKeyUpBang, "on_key_up!", "Key release event", - nullptr, 0, P2_KEY_UP_OUT, 3, true, false}, + // on_key_up! — removed + {NodeTypeID::OnKeyUpBang, "on_key_up!", "(removed)", + nullptr, 0, nullptr, 0}, // select!: bang + condition, 3 bang outputs {NodeTypeID::SelectBang, "select!", "Branch on condition", - P2_SELECT_BANG_IN, 2, P2_SELECT_BANG_OUT, 3, false, false}, + P2_SELECT_BANG_IN, 2, P2_SELECT_BANG_OUT, 3, NodeKind2::Banged}, // expr!: bang input, next + outputs (dynamic) {NodeTypeID::ExprBang, "expr!", "Evaluate expression on bang", - P2_EXPR_BANG_IN, 1, P2_NEXT, 1, false, false}, + P2_EXPR_BANG_IN, 1, P2_NEXT, 1, NodeKind2::Banged}, // erase!: bang + 2 inputs, next + result {NodeTypeID::EraseBang, "erase!", "Erase from collection (bang)", - P2_ERASE_BANG_IN, 3, P2_NEXT_RESULT, 2, false, false}, + P2_ERASE_BANG_IN, 3, P2_NEXT_RESULT, 2, NodeKind2::Banged}, // iterate: collection + fn, no outputs {NodeTypeID::Iterate, "iterate", "Iterate collection", - P2_ITERATE_IN, 2, nullptr, 0, false, false}, + P2_ITERATE_IN, 2, nullptr, 0}, // iterate!: bang + collection + fn, next {NodeTypeID::IterateBang, "iterate!", "Iterate collection (bang)", - P2_ITERATE_BANG_IN, 3, P2_NEXT, 1, false, false}, + P2_ITERATE_BANG_IN, 3, P2_NEXT, 1, NodeKind2::Banged}, // next: 1 input, 1 output {NodeTypeID::Next, "next", "Advance iterator", - P2_VALUE, 1, P2_RESULT, 1, false, false}, + P2_VALUE, 1, P2_RESULT, 1}, // lock: mutex + fn fixed inputs + va_args params, no outputs {NodeTypeID::Lock, "lock", "Execute under mutex lock", - P2_LOCK_IN, 2, nullptr, 0, false, false, &P2_VA_PARAM}, + P2_LOCK_IN, 2, nullptr, 0, NodeKind2::Flow, &P2_VA_PARAM}, // lock!: bang + mutex + fn fixed inputs + va_args params, next {NodeTypeID::LockBang, "lock!", "Execute under mutex lock (bang)", - P2_LOCK_BANG_IN, 3, P2_NEXT, 1, false, false, &P2_VA_PARAM}, + P2_LOCK_BANG_IN, 3, P2_NEXT, 1, NodeKind2::Banged, &P2_VA_PARAM}, // resize!: bang + target + size, next {NodeTypeID::ResizeBang, "resize!", "Resize vector", - P2_RESIZE_IN, 3, P2_NEXT, 1, false, false}, + P2_RESIZE_IN, 3, P2_NEXT, 1, NodeKind2::Banged}, // cast: 1 input, 1 output {NodeTypeID::Cast, "cast", "Cast value to type", - P2_VALUE, 1, P2_RESULT, 1, false, false}, + P2_VALUE, 1, P2_RESULT, 1}, // label: no pins {NodeTypeID::Label, "label", "Text label", - nullptr, 0, nullptr, 0, false, false}, + nullptr, 0, nullptr, 0}, // deref: 1 input, 1 output {NodeTypeID::Deref, "deref", "Dereference iterator (internal)", - P2_VALUE, 1, P2_RESULT, 1, false, false}, + P2_VALUE, 1, P2_RESULT, 1}, // error: no pins {NodeTypeID::Error, "error", "Error: invalid node", - nullptr, 0, nullptr, 0, false, false}, + nullptr, 0, nullptr, 0}, }; static constexpr int NUM_NODE_TYPES2 = sizeof(NODE_TYPES2) / sizeof(NODE_TYPES2[0]); From 1855ab542da2f4e61938185835a327b15eefd6d7 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 03:16:56 +0200 Subject: [PATCH 21/86] node drawing: add side-bang and lambda grab only for flow nodes --- src/attoflow/editor2.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 1c0267a..06f2f61 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -369,8 +369,9 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil } } - // Lambda grab handle (left-pointing triangle, middle-left) - { + // Flow-only: lambda grab (left) and side-bang (right) + if (nt->is_flow()) { + // Lambda grab handle (left-pointing triangle, middle-left) ImVec2 gp = layout.lambda_grab_pos(); ImU32 lc = IM_COL32(180, 130, 255, 255); dl->AddTriangleFilled( @@ -378,6 +379,10 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil {gp.x - pr, gp.y}, {gp.x + pr, gp.y + pr}, lc); + + // Side-bang (square, middle-right) + ImVec2 bp = {layout.pos.x + layout.width, layout.pos.y + layout.height * 0.5f}; + dl->AddRectFilled({bp.x - pr, bp.y - pr}, {bp.x + pr, bp.y + pr}, COL_PIN_BANG); } // Hover tooltip: show parsed_args, parsed_va_args, remaps From db492f7ea72547805f1988ca2689410c42ab98fc Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 03:29:13 +0200 Subject: [PATCH 22/86] better output mappings --- src/atto/graphbuilder.cpp | 96 +++++++++++++++++++++++++++++++++++---- src/atto/graphbuilder.h | 2 + src/attoflow/editor2.cpp | 32 ++++++++++--- 3 files changed, 114 insertions(+), 16 deletions(-) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index 01f2e1c..e25d98a 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -386,15 +386,93 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { auto node_entry = gb->find(cur_id); - // Wire nets from outputs (this node is source) - // Skip -as_lambda entries — in v1, lambda captures reference nodes directly - for (auto& net_name : cur_outputs) { - if (net_name.empty()) continue; - if (net_name.size() > 10 && net_name.compare(net_name.size() - 10, 10, "-as_lambda") == 0) - continue; - auto [_, net_ptr] = gb->find_or_create_net(net_name, true); - if (net_ptr && std::holds_alternative(*net_ptr)) - std::get(*net_ptr).source = node_entry; + // Wire nets from outputs — smart map old positions to new descriptor order + // Old outputs array: [nexts..., data_outs..., post_bang, lambda_grab] + // New: nb.outputs[i] = ArgNet2 for new output_ports[i] + { + auto* old_nt = find_node_type(cur_type.c_str()); + auto* new_nt = find_node_type2(nb.type_id); + bool is_expr = is_any_of(nb.type_id, NodeTypeID::Expr, NodeTypeID::ExprBang); + int old_num_nexts = old_nt ? old_nt->num_nexts : 0; + + // Helper: wire a net and return ArgNet2 + auto wire_output = [&](const std::string& net_name) -> ArgNet2 { + auto [resolved, net_ptr] = gb->find_or_create_net(net_name, true); + if (net_ptr && std::holds_alternative(*net_ptr)) + std::get(*net_ptr).source = node_entry; + return {resolved, net_ptr}; + }; + + // Filter out empty and -as_lambda entries, wire all nets + // For expr: outputs are all data (no nexts), dynamic count + // For others: [nexts..., data_outs..., post_bang] + if (is_expr) { + // Expr: positional mapping — all outputs are data, last may be post_bang + for (int i = 0; i < (int)cur_outputs.size(); i++) { + auto& net_name = cur_outputs[i]; + if (net_name.empty()) continue; + // Check for post_bang suffix + bool is_post_bang = (net_name.size() > 10 && + net_name.compare(net_name.size() - 10, 10, "-post_bang") == 0); + if (net_name.size() > 10 && net_name.compare(net_name.size() - 10, 10, "-as_lambda") == 0) + continue; + auto arg = wire_output(net_name); + // post_bang is side-bang for flow nodes — don't add to outputs array + // (it's implicit from NodeKind2::Flow) + if (!is_post_bang) { + while ((int)nb.outputs.size() <= i) + nb.outputs.push_back({"$unconnected", gb->find("$unconnected")}); + nb.outputs[i] = std::move(arg); + } + } + } else { + // Name-based mapping for non-expr nodes + int old_num_outs = old_nt ? old_nt->outputs : 0; + std::map out_net_map; + + for (int i = 0; i < (int)cur_outputs.size(); i++) { + auto& net_name = cur_outputs[i]; + if (net_name.empty()) continue; + if (net_name.size() > 10 && net_name.compare(net_name.size() - 10, 10, "-as_lambda") == 0) + continue; + + auto arg = wire_output(net_name); + + // Determine old pin name from position + std::string old_pin_name; + if (i < old_num_nexts) { + old_pin_name = (old_nt && old_nt->next_ports) ? old_nt->next_ports[i].name : "bang"; + } else if (i < old_num_nexts + old_num_outs) { + int out_idx = i - old_num_nexts; + old_pin_name = (old_nt && old_nt->output_ports) ? old_nt->output_ports[out_idx].name : "result"; + } else { + old_pin_name = "post_bang"; + } + + out_net_map[old_pin_name] = std::move(arg); + } + + // Map to new descriptor order + if (new_nt) { + nb.outputs.resize(new_nt->num_outputs); + auto unconnected = gb->find("$unconnected"); + for (int i = 0; i < new_nt->num_outputs; i++) { + const char* name = new_nt->output_ports[i].name; + auto it = out_net_map.find(name); + if (it != out_net_map.end()) { + nb.outputs[i] = std::move(it->second); + } else if (strcmp(name, "next") == 0) { + auto it2 = out_net_map.find("bang"); + if (it2 != out_net_map.end()) + nb.outputs[i] = std::move(it2->second); + else + nb.outputs[i] = {"$unconnected", unconnected}; + } else { + nb.outputs[i] = {"$unconnected", unconnected}; + } + } + } + } } // ─── v0 → v1 port mapping: merge inputs + args by port name ─── diff --git a/src/atto/graphbuilder.h b/src/atto/graphbuilder.h index 8ab7b8b..c5b149b 100644 --- a/src/atto/graphbuilder.h +++ b/src/atto/graphbuilder.h @@ -65,6 +65,7 @@ struct NetBuilder { // Remap: $N → net mapping (from folded shadow inputs) using Remaps = std::vector; +using Outputs = std::vector; // A node under construction — holds structured parsed args instead of raw string. struct FlowNodeBuilder { @@ -72,6 +73,7 @@ struct FlowNodeBuilder { std::shared_ptr parsed_args; // base pins (1:1 with descriptor) std::shared_ptr parsed_va_args; // va_args pins Remaps remaps; // $N → net mapping (remaps[0] = net for $0, etc.) + Outputs outputs; // output pin → net mapping (1:1 with descriptor output_ports) Vec2 position = {0, 0}; bool shadow = false; // only used during migration, must be false after folding std::string error; diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 06f2f61..fe8ee52 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -186,6 +186,7 @@ void Editor2Pane::draw() { FlowNodeBuilder* src_node_ptr = nullptr; bool named = false; bool is_lambda = false; + int source_pin = 0; if (std::holds_alternative(*entry)) { auto& net = std::get(*entry); @@ -194,32 +195,49 @@ void Editor2Pane::draw() { if (!src_ptr || !std::holds_alternative(*src_ptr)) return; src_node_ptr = &std::get(*src_ptr); named = !net.auto_wire; + // Find which output pin this net is on + for (int k = 0; k < (int)src_node_ptr->outputs.size(); k++) { + if (src_node_ptr->outputs[k].second == entry) { + source_pin = k; + break; + } + } } else if (std::holds_alternative(*entry)) { - // Lambda capture: entry IS the source node src_node_ptr = &std::get(*entry); is_lambda = true; } else { return; } + auto* src_nt = find_node_type2(src_node_ptr->type_id); auto src_layout = compute_node_layout(*src_node_ptr, canvas_origin, canvas_zoom_); ImVec2 to = dst_layout.input_pin_pos(dst_pin); float th = 2.5f * canvas_zoom_; ImVec2 from; if (is_lambda) { - // Lambda wire: from grab (middle-left) with horizontal-then-vertical curve from = src_layout.lambda_grab_pos(); float dx = std::max(std::abs(to.x - from.x) * 0.5f, 30.0f * canvas_zoom_); float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); ImU32 col = IM_COL32(180, 130, 255, 200); dl->AddBezierCubic(from, {from.x - dx, from.y}, {to.x, to.y - dy}, to, col, th); } else { - // Regular wire: from output pin (bottom) with vertical curve - from = src_layout.output_pin_pos(0); // TODO: track output pin index - ImU32 col = named ? IM_COL32(200, 200, 100, 120) : COL_LINK; - float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); - dl->AddBezierCubic(from, {from.x, from.y + dy}, {to.x, to.y - dy}, to, col, th); + bool is_side_bang = src_nt && src_nt->is_flow() && + source_pin < (src_nt->num_outputs) && + src_nt->output_ports && src_nt->output_ports[source_pin].kind == PortKind2::BangNext; + + if (is_side_bang) { + from = {src_layout.pos.x + src_layout.width, src_layout.pos.y + src_layout.height * 0.5f}; + float dx = std::max(std::abs(to.x - from.x) * 0.5f, 30.0f * canvas_zoom_); + float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); + ImU32 col = named ? IM_COL32(200, 200, 100, 120) : COL_LINK; + dl->AddBezierCubic(from, {from.x + dx, from.y}, {to.x, to.y - dy}, to, col, th); + } else { + from = src_layout.output_pin_pos(source_pin); + ImU32 col = named ? IM_COL32(200, 200, 100, 120) : COL_LINK; + float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); + dl->AddBezierCubic(from, {from.x, from.y + dy}, {to.x, to.y - dy}, to, col, th); + } } // Label for named nets From 21a2eabb081641c682ff4c6879e95f46e7c70e43 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 03:43:54 +0200 Subject: [PATCH 23/86] better imports --- src/atto/graphbuilder.cpp | 54 +++++++++++++++++++++++++++++++++------ src/attoflow/editor2.cpp | 45 +++++++++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 11 deletions(-) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index e25d98a..95d0fa8 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -508,9 +508,26 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { return {id, net_ptr}; }; - if (is_expr || !old_nt || !new_nt) { - // Expr nodes or unknown types: simple positional mapping - // inputs map directly as ArgNet2 entries prepended to parsed_args + if (is_expr) { + // Expr nodes: inputs map to $N remaps, not descriptor ports + // For expr!, inputs[0] is the bang trigger, rest are $N + int bang_offset = is_any_of(nb.type_id, NodeTypeID::ExprBang) ? 1 : 0; + for (int i = 0; i < (int)cur_inputs.size(); i++) { + auto arg = resolve_net(cur_inputs[i]); + if (i < bang_offset) { + // Bang trigger → prepend to parsed_args + if (!nb.parsed_args) nb.parsed_args = std::make_shared(); + nb.parsed_args->insert(nb.parsed_args->begin(), std::move(arg)); + } else { + // $N remap + int remap_idx = i - bang_offset; + while ((int)nb.remaps.size() <= remap_idx) + nb.remaps.push_back({"$unconnected", gb->find("$unconnected")}); + nb.remaps[remap_idx] = std::move(arg); + } + } + } else if (!old_nt || !new_nt) { + // Unknown types: simple positional prepend auto merged = std::make_shared(); for (auto& net_name : cur_inputs) merged->push_back(resolve_net(net_name)); @@ -651,6 +668,13 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { } } + // Ensure remaps are sized to rewrite_input_count (from $N refs in expressions) + if (nb.parsed_args && nb.parsed_args->rewrite_input_count > (int)nb.remaps.size()) { + auto unconnected = gb->find("$unconnected"); + while ((int)nb.remaps.size() < nb.parsed_args->rewrite_input_count) + nb.remaps.push_back({"$unconnected", unconnected}); + } + cur_id.clear(); cur_type.clear(); cur_args.clear(); cur_inputs.clear(); cur_outputs.clear(); cur_x = 0; cur_y = 0; cur_shadow = false; @@ -740,12 +764,26 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if (!shadow_ptr) continue; auto& shadow = std::get(*shadow_ptr); - // Insert shadow expression into parent's parsed_args at arg_index - if (parent.parsed_args) { - while ((int)parent.parsed_args->size() <= arg_index) - parent.parsed_args->push_back(ArgString2{""}); - if (shadow.parsed_args && !shadow.parsed_args->empty()) + // Insert shadow expression into parent's parsed_args + // Find the shadow's output net (e.g. "$auto-xxx_s0-out0") in parent's parsed_args and replace + if (parent.parsed_args && shadow.parsed_args && !shadow.parsed_args->empty()) { + std::string shadow_out_prefix = shadow_id + "-out"; + bool replaced = false; + for (auto& a : *parent.parsed_args) { + if (auto* an = std::get_if(&a)) { + if (an->first.compare(0, shadow_out_prefix.size(), shadow_out_prefix) == 0) { + a = (*shadow.parsed_args)[0]; + replaced = true; + break; + } + } + } + // Fallback: try positional insertion (for nodes without merged inputs) + if (!replaced) { + while ((int)parent.parsed_args->size() <= arg_index) + parent.parsed_args->push_back(ArgString2{""}); (*parent.parsed_args)[arg_index] = (*shadow.parsed_args)[0]; + } } // Build remaps from saved shadow input nets diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index fe8ee52..bb232c3 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -69,8 +69,10 @@ static NodeLayout compute_node_layout(const FlowNodeBuilder& node, ImVec2 canvas for (auto& a : *pa) if (std::holds_alternative(a)) n++; return n; }; + bool has_va = nt && nt->va_args != nullptr; int num_in = count_net_args(node.parsed_args.get()) + count_net_args(node.parsed_va_args.get()) + + (has_va ? 1 : 0) // +1 for the "add more" diamond + (int)node.remaps.size(); int num_out = nt ? nt->num_outputs : 1; if (node.type_id == NodeTypeID::Expr || node.type_id == NodeTypeID::ExprBang) { @@ -274,7 +276,11 @@ void Editor2Pane::draw() { pin += draw_wires_from_args(dst_node.parsed_args.get(), pin); pin += draw_wires_from_args(dst_node.parsed_va_args.get(), pin); - // Remaps: $N pins (appended after base + va_args) + // Skip +diamond slot if node has va_args + auto* dst_nt2 = find_node_type2(dst_node.type_id); + if (dst_nt2 && dst_nt2->va_args) pin++; + + // Remaps: $N pins (appended after base + va_args + add-diamond) for (int i = 0; i < (int)dst_node.remaps.size(); i++) { auto& remap = dst_node.remaps[i]; draw_wire_to_pin(pin + i, remap.second, remap.first); @@ -345,6 +351,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil // Draw input pins (top) // Pin order: parsed_args ArgNet2 (inputs merged first), then va_args ArgNet2, then remaps float pr = PIN_RADIUS * canvas_zoom_; + bool has_va = nt->va_args != nullptr; auto count_net_args_in = [](const ParsedArgs2* pa) -> int { if (!pa) return 0; int n = 0; @@ -353,21 +360,53 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil }; int base_pin_count = count_net_args_in(node.parsed_args.get()); int va_pin_count = count_net_args_in(node.parsed_va_args.get()); + int add_pin_pos = base_pin_count + va_pin_count; // where the +diamond goes (if va_args) for (int i = 0; i < layout.num_in; i++) { ImVec2 pp = layout.input_pin_pos(i); + + // Draw the "add more" diamond at the boundary between va_args and remaps + if (has_va && i == add_pin_pos) { + ImU32 pc = IM_COL32(120, 120, 140, 180); + dl->AddQuadFilled({pp.x, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x, pp.y + pr}, {pp.x - pr, pp.y}, pc); + float cr = pr * 0.5f; + ImU32 tc = IM_COL32(200, 200, 220, 220); + float lth = 1.5f * canvas_zoom_; + dl->AddLine({pp.x - cr, pp.y}, {pp.x + cr, pp.y}, tc, lth); + dl->AddLine({pp.x, pp.y - cr}, {pp.x, pp.y + cr}, tc, lth); + continue; + } + PortKind2 kind = PortKind2::Data; + bool is_va = false; + bool is_optional = false; if (i < base_pin_count && nt->input_ports && i < nt->num_inputs) { kind = nt->input_ports[i].kind; - } else if (i >= base_pin_count && i < base_pin_count + va_pin_count) { + is_optional = nt->input_ports[i].optional; + } else if (i >= base_pin_count && i < add_pin_pos) { kind = nt->va_args ? nt->va_args->kind : PortKind2::Data; + is_va = true; } + // pins after add_pin_pos are remaps (Data) ImU32 pc = pin_color(kind); if (kind == PortKind2::BangTrigger) { dl->AddRectFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y + pr}, pc); } else if (kind == PortKind2::Lambda) { - // Down-pointing triangle for lambda inputs (matching editor1) dl->AddTriangleFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y - pr}, {pp.x, pp.y + pr}, pc); + } else if (is_va) { + // Diamond for va_args pins + dl->AddQuadFilled({pp.x, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x, pp.y + pr}, {pp.x - pr, pp.y}, pc); + } else if (is_optional) { + // Diamond with ? for optional pins + dl->AddQuadFilled({pp.x, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x, pp.y + pr}, {pp.x - pr, pp.y}, pc); + float font_sz = pr * 1.6f; + if (font_sz > 3.0f) { + ImVec2 ts = ImGui::CalcTextSize("?"); + float scale = font_sz / ImGui::GetFontSize(); + dl->AddText(nullptr, font_sz, + {pp.x - ts.x * scale * 0.5f, pp.y - ts.y * scale * 0.5f}, + IM_COL32(30, 30, 40, 255), "?"); + } } else { dl->AddCircleFilled(pp, pr, pc); } From d2482e2346e0b814a052cb7c7ed286ea47b0b5b5 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 03:50:08 +0200 Subject: [PATCH 24/86] Add special handling for label and error nodes in editor rendering --- src/atto/node_types2.h | 6 ++++-- src/attoflow/editor2.cpp | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/atto/node_types2.h b/src/atto/node_types2.h index 9bda1e0..eedc9e6 100644 --- a/src/atto/node_types2.h +++ b/src/atto/node_types2.h @@ -23,6 +23,7 @@ enum class NodeKind2 : uint8_t { Banged, // bang trigger input (top) + bang next output (bottom) Event, // event source — bang next output (bottom), no bang input Declaration, // compile-time — bang trigger input (top) + bang next output (bottom) + Special, // Label or Error - special handling }; struct NodeType2 { @@ -39,6 +40,7 @@ struct NodeType2 { bool is_banged() const { return kind == NodeKind2::Banged || kind == NodeKind2::Event || kind == NodeKind2::Declaration; } bool is_declaration() const { return kind == NodeKind2::Declaration; } bool is_flow() const { return kind == NodeKind2::Flow; } + bool is_special() const { return kind == NodeKind2::Special; } bool is_event() const { return kind == NodeKind2::Event; } }; @@ -357,7 +359,7 @@ static const NodeType2 NODE_TYPES2[] = { // label: no pins {NodeTypeID::Label, "label", "Text label", - nullptr, 0, nullptr, 0}, + nullptr, 0, nullptr, 0, NodeKind2::Special}, // deref: 1 input, 1 output {NodeTypeID::Deref, "deref", "Dereference iterator (internal)", @@ -365,7 +367,7 @@ static const NodeType2 NODE_TYPES2[] = { // error: no pins {NodeTypeID::Error, "error", "Error: invalid node", - nullptr, 0, nullptr, 0}, + nullptr, 0, nullptr, 0, NodeKind2::Special}, }; static constexpr int NUM_NODE_TYPES2 = sizeof(NODE_TYPES2) / sizeof(NODE_TYPES2[0]); diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index bb232c3..e074dd7 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -324,6 +324,39 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil auto layout = compute_node_layout(node, canvas_origin, canvas_zoom_); + // Special nodes: label and error + if (nt->is_special()) { + // Display first arg without quotes + std::string display; + if (node.parsed_args && !node.parsed_args->empty()) { + auto& a = (*node.parsed_args)[0]; + if (auto* s = std::get_if(&a)) display = s->value; + else if (auto* e = std::get_if(&a)) display = e->expr; + else display = node.args_str(); + } + + float font_size = ImGui::GetFontSize() * canvas_zoom_; + bool is_error = (node.type_id == NodeTypeID::Error); + + if (is_error) { + // Error: red box + dl->AddRectFilled(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, + COL_NODE_ERR, 4.0f * canvas_zoom_); + dl->AddRect(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, + IM_COL32(255, 80, 80, 255), 4.0f * canvas_zoom_); + } + // Label: no box at all + + if (font_size > 5.0f) { + ImVec2 text_sz = ImGui::CalcTextSize(display.c_str()); + float tw = text_sz.x * canvas_zoom_; + float cx = layout.pos.x + (layout.width - tw) * 0.5f; + float cy = layout.pos.y + (layout.height - font_size) * 0.5f; + dl->AddText(nullptr, font_size, {cx, cy}, COL_TEXT, display.c_str()); + } + return; // no pins for special nodes + } + // Display text std::string display = nt->name; std::string args = node.args_str(); From fe361ca629ab1004018af3ba347d039497224fe7 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 10:57:26 +0200 Subject: [PATCH 25/86] Add initial value support to decl_var input ports --- src/atto/node_types2.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/atto/node_types2.h b/src/atto/node_types2.h index eedc9e6..2b1e43e 100644 --- a/src/atto/node_types2.h +++ b/src/atto/node_types2.h @@ -170,6 +170,7 @@ static const PortDesc2 P2_DECL_VAR_IN[] = { {"bang_in", "trigger", PortKind2::BangTrigger}, {"name", "variable name (symbol)"}, {"type", "variable type"}, + {"initial", "variable initial value", PortKind2::Data, nullptr, true}, }; static const PortDesc2 P2_DECL_VAR_OUT[] = { {"next", "fires after declaration", PortKind2::BangNext}, @@ -255,7 +256,7 @@ static const NodeType2 NODE_TYPES2[] = { // decl_var {NodeTypeID::DeclVar, "decl_var", "Declare a variable", - P2_DECL_VAR_IN, 3, P2_DECL_VAR_OUT, 2, NodeKind2::Declaration}, + P2_DECL_VAR_IN, 4, P2_DECL_VAR_OUT, 2, NodeKind2::Declaration}, // decl {NodeTypeID::Decl, "decl", "Compile-time entry point", From a2a06542e32ad42a490e5afb96f1eb6e185a35f4 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 11:03:56 +0200 Subject: [PATCH 26/86] editor2: pin hovers --- src/attoflow/editor2.cpp | 155 ++++++++++++++++++++++++++++++++++++++- src/attoflow/editor2.h | 2 + 2 files changed, 155 insertions(+), 2 deletions(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index e074dd7..eb5b525 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -289,6 +289,43 @@ void Editor2Pane::draw() { dl->PopClipRect(); + // ─── Node dragging with left mouse ─── + if (canvas_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + // Hit test: find node under mouse + ImVec2 mouse = ImGui::GetIO().MousePos; + dragging_node_.clear(); + // Iterate in reverse so topmost (last drawn) is hit first + for (auto it = gb_->entries.rbegin(); it != gb_->entries.rend(); ++it) { + if (!std::holds_alternative(*it->second)) continue; + auto& node = std::get(*it->second); + auto layout = compute_node_layout(node, canvas_origin, canvas_zoom_); + if (mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && + mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height) { + dragging_node_ = it->first; + selected_nodes_.clear(); + selected_nodes_.insert(dragging_node_); + dragging_started_ = true; + break; + } + } + if (dragging_node_.empty()) { + selected_nodes_.clear(); + } + } + if (dragging_started_ && ImGui::IsMouseDragging(ImGuiMouseButton_Left) && !dragging_node_.empty()) { + auto it = gb_->entries.find(dragging_node_); + if (it != gb_->entries.end() && std::holds_alternative(*it->second)) { + auto& node = std::get(*it->second); + ImVec2 delta = ImGui::GetIO().MouseDelta; + node.position.x += delta.x / canvas_zoom_; + node.position.y += delta.y / canvas_zoom_; + } + } + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + dragging_node_.clear(); + dragging_started_ = false; + } + // Pan with middle mouse or right mouse if (canvas_hovered && ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) { canvas_offset_.x += ImGui::GetIO().MouseDelta.x; @@ -475,9 +512,123 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil dl->AddRectFilled({bp.x - pr, bp.y - pr}, {bp.x + pr, bp.y + pr}, COL_PIN_BANG); } - // Hover tooltip: show parsed_args, parsed_va_args, remaps + // Pin hover: highlight + tooltip ImVec2 mouse = ImGui::GetMousePos(); - if (mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && + float hit_r = pr * 2.5f; + auto dist2 = [](ImVec2 a, ImVec2 b) { return (a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y); }; + float hit_r2 = hit_r * hit_r; + float ho = 2.0f * canvas_zoom_; // highlight outline offset + ImU32 COL_HOVER = IM_COL32(255, 255, 255, 255); + + // Helper to get input pin name + auto get_input_pin_name = [&](int i) -> const char* { + int adj = (has_va && i > add_pin_pos) ? i - 1 : i; + if (adj < base_pin_count && nt->input_ports && adj < nt->num_inputs) + return nt->input_ports[adj].name; + if (adj >= base_pin_count && adj < add_pin_pos && nt->va_args) + return nt->va_args->name; + int remap_idx = adj - add_pin_pos; + static char remap_buf[16]; + snprintf(remap_buf, sizeof(remap_buf), "$%d", remap_idx); + return remap_buf; + }; + + // Helper to get input pin shape for highlight + enum class PinShape2 { Circle, Square, Diamond, TriangleDown, TriangleLeft }; + auto get_input_pin_shape = [&](int i) -> PinShape2 { + if (has_va && i == add_pin_pos) return PinShape2::Diamond; + int adj = (has_va && i > add_pin_pos) ? i - 1 : i; + PortKind2 kind = PortKind2::Data; + bool is_va_pin = false; + bool is_opt = false; + if (adj < base_pin_count && nt->input_ports && adj < nt->num_inputs) { + kind = nt->input_ports[adj].kind; + is_opt = nt->input_ports[adj].optional; + } else if (adj >= base_pin_count && adj < add_pin_pos) { + is_va_pin = true; + } + if (kind == PortKind2::BangTrigger) return PinShape2::Square; + if (kind == PortKind2::Lambda) return PinShape2::TriangleDown; + if (is_va_pin || is_opt) return PinShape2::Diamond; + return PinShape2::Circle; + }; + + auto draw_highlight = [&](ImVec2 pos, PinShape2 shape) { + switch (shape) { + case PinShape2::Circle: + dl->AddCircle(pos, pr + ho, COL_HOVER, 0, 2.0f); + break; + case PinShape2::Square: + dl->AddRect({pos.x - pr - ho, pos.y - pr - ho}, {pos.x + pr + ho, pos.y + pr + ho}, COL_HOVER, 0, 0, 2.0f); + break; + case PinShape2::Diamond: + dl->AddQuad({pos.x, pos.y - pr - ho}, {pos.x + pr + ho, pos.y}, {pos.x, pos.y + pr + ho}, {pos.x - pr - ho, pos.y}, COL_HOVER, 2.0f); + break; + case PinShape2::TriangleDown: + dl->AddTriangle({pos.x - pr - ho, pos.y - pr - ho}, {pos.x + pr + ho, pos.y - pr - ho}, {pos.x, pos.y + pr + ho}, COL_HOVER, 2.0f); + break; + case PinShape2::TriangleLeft: + dl->AddTriangle({pos.x + pr + ho, pos.y - pr - ho}, {pos.x - pr - ho, pos.y}, {pos.x + pr + ho, pos.y + pr + ho}, COL_HOVER, 2.0f); + break; + } + }; + + bool pin_hovered = false; + // Check input pins + for (int i = 0; i < layout.num_in; i++) { + if (has_va && i == add_pin_pos) continue; + ImVec2 pp = layout.input_pin_pos(i); + if (dist2(mouse, pp) < hit_r2) { + draw_highlight(pp, get_input_pin_shape(i)); + ImGui::BeginTooltip(); + ImGui::Text("%s", get_input_pin_name(i)); + ImGui::EndTooltip(); + pin_hovered = true; + break; + } + } + // Check output pins + if (!pin_hovered) { + for (int i = 0; i < layout.num_out; i++) { + ImVec2 pp = layout.output_pin_pos(i); + if (dist2(mouse, pp) < hit_r2) { + PortKind2 kind = (nt->output_ports && i < nt->num_outputs) ? nt->output_ports[i].kind : PortKind2::Data; + draw_highlight(pp, kind == PortKind2::BangNext ? PinShape2::Square : PinShape2::Circle); + const char* name = (nt->output_ports && i < nt->num_outputs) ? nt->output_ports[i].name : "out"; + ImGui::BeginTooltip(); + ImGui::Text("%s", name); + ImGui::EndTooltip(); + pin_hovered = true; + break; + } + } + } + // Check lambda grab + if (!pin_hovered && nt->is_flow()) { + ImVec2 gp = layout.lambda_grab_pos(); + if (dist2(mouse, gp) < hit_r2) { + draw_highlight(gp, PinShape2::TriangleLeft); + ImGui::BeginTooltip(); + ImGui::Text("as_lambda"); + ImGui::EndTooltip(); + pin_hovered = true; + } + } + // Check side-bang + if (!pin_hovered && nt->is_flow()) { + ImVec2 bp = {layout.pos.x + layout.width, layout.pos.y + layout.height * 0.5f}; + if (dist2(mouse, bp) < hit_r2) { + draw_highlight(bp, PinShape2::Square); + ImGui::BeginTooltip(); + ImGui::Text("post_bang"); + ImGui::EndTooltip(); + pin_hovered = true; + } + } + + // Node tooltip (when hovering body, not a pin) + if (!pin_hovered && + mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height) { ImGui::BeginTooltip(); ImGui::Text("id: %s", id.c_str()); diff --git a/src/attoflow/editor2.h b/src/attoflow/editor2.h index 4dafd37..f66a95e 100644 --- a/src/attoflow/editor2.h +++ b/src/attoflow/editor2.h @@ -33,6 +33,8 @@ class Editor2Pane { // Interaction state std::set selected_nodes_; + NodeId dragging_node_; // node being dragged (empty = none) + bool dragging_started_ = false; int editing_link_id_ = -1; // not used yet, placeholder // Drawing helpers From 7a730908e18db66f40adb7417e6fb52192f322f3 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 11:10:01 +0200 Subject: [PATCH 27/86] editor2: move styles out --- src/attoflow/editor2.cpp | 151 ++++++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 58 deletions(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index eb5b525..d2f329d 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -9,23 +9,57 @@ #include #include -// ─── Constants ─── - -static constexpr float NODE_MIN_WIDTH = 80.0f; -static constexpr float NODE_HEIGHT = 40.0f; -static constexpr float PIN_RADIUS = 5.0f; -static constexpr float PIN_SPACING = 16.0f; - -static constexpr ImU32 COL_BG = IM_COL32(30, 30, 40, 255); -static constexpr ImU32 COL_GRID = IM_COL32(50, 50, 60, 255); -static constexpr ImU32 COL_NODE = IM_COL32(50, 55, 75, 220); -static constexpr ImU32 COL_NODE_SEL = IM_COL32(80, 90, 130, 255); -static constexpr ImU32 COL_NODE_ERR = IM_COL32(130, 40, 40, 220); -static constexpr ImU32 COL_TEXT = IM_COL32(220, 220, 220, 255); -static constexpr ImU32 COL_PIN_DATA = IM_COL32(100, 200, 100, 255); -static constexpr ImU32 COL_PIN_BANG = IM_COL32(255, 200, 80, 255); -static constexpr ImU32 COL_PIN_LAMBDA= IM_COL32(180, 130, 255, 255); -static constexpr ImU32 COL_LINK = IM_COL32(200, 200, 100, 200); +// ─── Style ─── + +static struct { + // Layout + float node_min_width = 80.0f; + float node_height = 40.0f; + float pin_radius = 5.0f; + float pin_spacing = 16.0f; + float node_rounding = 4.0f; + float grid_step = 20.0f; + + // Thickness + float wire_thickness = 2.5f; + float node_border = 1.0f; // implicit from AddRect + float highlight_offset = 2.0f; + float highlight_thickness = 2.0f; + float add_pin_line = 1.5f; + + // Hit testing + float pin_hit_radius_mul = 2.5f; + + // Canvas colors + ImU32 col_bg = IM_COL32(30, 30, 40, 255); + ImU32 col_grid = IM_COL32(50, 50, 60, 255); + + // Node colors + ImU32 col_node = IM_COL32(50, 55, 75, 220); + ImU32 col_node_sel = IM_COL32(80, 90, 130, 255); + ImU32 col_node_err = IM_COL32(130, 40, 40, 220); + ImU32 col_node_border = IM_COL32(80, 80, 100, 255); + ImU32 col_err_border = IM_COL32(255, 80, 80, 255); + ImU32 col_text = IM_COL32(220, 220, 220, 255); + + // Pin colors + ImU32 col_pin_data = IM_COL32(100, 200, 100, 255); + ImU32 col_pin_bang = IM_COL32(255, 200, 80, 255); + ImU32 col_pin_lambda = IM_COL32(180, 130, 255, 255); + ImU32 col_pin_hover = IM_COL32(255, 255, 255, 255); + ImU32 col_add_pin = IM_COL32(120, 120, 140, 180); + ImU32 col_add_pin_fg = IM_COL32(200, 200, 220, 220); + ImU32 col_opt_pin_fg = IM_COL32(30, 30, 40, 255); + + // Wire colors + ImU32 col_wire = IM_COL32(200, 200, 100, 200); + ImU32 col_wire_named = IM_COL32(200, 200, 100, 120); + ImU32 col_wire_lambda = IM_COL32(180, 130, 255, 200); + + // Net label colors + ImU32 col_label_bg = IM_COL32(30, 30, 40, 200); + ImU32 col_label_text = IM_COL32(180, 220, 255, 255); +} S; // ─── Helpers ─── @@ -43,10 +77,10 @@ struct NodeLayout { float zoom; ImVec2 input_pin_pos(int i) const { - return {pos.x + (i + 0.5f) * PIN_SPACING * zoom, pos.y}; + return {pos.x + (i + 0.5f) * S.pin_spacing * zoom, pos.y}; } ImVec2 output_pin_pos(int i) const { - return {pos.x + (i + 0.5f) * PIN_SPACING * zoom, pos.y + height}; + return {pos.x + (i + 0.5f) * S.pin_spacing * zoom, pos.y + height}; } ImVec2 lambda_grab_pos() const { return {pos.x, pos.y + height * 0.5f}; @@ -81,10 +115,10 @@ static NodeLayout compute_node_layout(const FlowNodeBuilder& node, ImVec2 canvas else num_out = std::max(1, args_count); } - float pin_w_top = std::max(0, num_in) * PIN_SPACING * zoom; - float pin_w_bot = std::max(0, num_out) * PIN_SPACING * zoom; - float node_w = std::max({NODE_MIN_WIDTH * zoom, text_w, pin_w_top, pin_w_bot}); - float node_h = NODE_HEIGHT * zoom; + float pin_w_top = std::max(0, num_in) * S.pin_spacing * zoom; + float pin_w_bot = std::max(0, num_out) * S.pin_spacing * zoom; + float node_w = std::max({S.node_min_width * zoom, text_w, pin_w_top, pin_w_bot}); + float node_h = S.node_height * zoom; ImVec2 pos = {canvas_origin.x + node.position.x * zoom, canvas_origin.y + node.position.y * zoom}; @@ -95,9 +129,9 @@ static NodeLayout compute_node_layout(const FlowNodeBuilder& node, ImVec2 canvas static ImU32 pin_color(PortKind2 kind) { switch (kind) { case PortKind2::BangTrigger: - case PortKind2::BangNext: return COL_PIN_BANG; - case PortKind2::Lambda: return COL_PIN_LAMBDA; - default: return COL_PIN_DATA; + case PortKind2::BangNext: return S.col_pin_bang; + case PortKind2::Lambda: return S.col_pin_lambda; + default: return S.col_pin_data; } } @@ -147,17 +181,17 @@ void Editor2Pane::draw() { ImDrawList* dl = ImGui::GetWindowDrawList(); // Background - dl->AddRectFilled(canvas_p0, v2add(canvas_p0, canvas_sz), COL_BG); + dl->AddRectFilled(canvas_p0, v2add(canvas_p0, canvas_sz), S.col_bg); ImVec2 canvas_origin = v2add(canvas_p0, canvas_offset_); // Grid - float grid_step = 20.0f * canvas_zoom_; + float grid_step = S.grid_step * canvas_zoom_; if (grid_step > 5.0f) { for (float x = fmodf(canvas_offset_.x, grid_step); x < canvas_sz.x; x += grid_step) - dl->AddLine({canvas_p0.x + x, canvas_p0.y}, {canvas_p0.x + x, canvas_p0.y + canvas_sz.y}, COL_GRID); + dl->AddLine({canvas_p0.x + x, canvas_p0.y}, {canvas_p0.x + x, canvas_p0.y + canvas_sz.y}, S.col_grid); for (float y = fmodf(canvas_offset_.y, grid_step); y < canvas_sz.y; y += grid_step) - dl->AddLine({canvas_p0.x, canvas_p0.y + y}, {canvas_p0.x + canvas_sz.x, canvas_p0.y + y}, COL_GRID); + dl->AddLine({canvas_p0.x, canvas_p0.y + y}, {canvas_p0.x + canvas_sz.x, canvas_p0.y + y}, S.col_grid); } // Clip @@ -214,14 +248,14 @@ void Editor2Pane::draw() { auto* src_nt = find_node_type2(src_node_ptr->type_id); auto src_layout = compute_node_layout(*src_node_ptr, canvas_origin, canvas_zoom_); ImVec2 to = dst_layout.input_pin_pos(dst_pin); - float th = 2.5f * canvas_zoom_; + float th = S.wire_thickness * canvas_zoom_; ImVec2 from; if (is_lambda) { from = src_layout.lambda_grab_pos(); float dx = std::max(std::abs(to.x - from.x) * 0.5f, 30.0f * canvas_zoom_); float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); - ImU32 col = IM_COL32(180, 130, 255, 200); + ImU32 col = S.col_wire_lambda; dl->AddBezierCubic(from, {from.x - dx, from.y}, {to.x, to.y - dy}, to, col, th); } else { bool is_side_bang = src_nt && src_nt->is_flow() && @@ -232,11 +266,11 @@ void Editor2Pane::draw() { from = {src_layout.pos.x + src_layout.width, src_layout.pos.y + src_layout.height * 0.5f}; float dx = std::max(std::abs(to.x - from.x) * 0.5f, 30.0f * canvas_zoom_); float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); - ImU32 col = named ? IM_COL32(200, 200, 100, 120) : COL_LINK; + ImU32 col = named ? S.col_wire_named : S.col_wire; dl->AddBezierCubic(from, {from.x + dx, from.y}, {to.x, to.y - dy}, to, col, th); } else { from = src_layout.output_pin_pos(source_pin); - ImU32 col = named ? IM_COL32(200, 200, 100, 120) : COL_LINK; + ImU32 col = named ? S.col_wire_named : S.col_wire; float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); dl->AddBezierCubic(from, {from.x, from.y + dy}, {to.x, to.y - dy}, to, col, th); } @@ -253,8 +287,8 @@ void Editor2Pane::draw() { float cx = mid.x - tw * 0.5f; float cy = mid.y - tth * 0.5f; dl->AddRectFilled({cx - 3, cy - 1}, {cx + tw + 3, cy + tth + 1}, - IM_COL32(30, 30, 40, 200), 3.0f); - dl->AddText(nullptr, font_size, {cx, cy}, IM_COL32(180, 220, 255, 255), net_id.c_str()); + S.col_label_bg, S.node_rounding); + dl->AddText(nullptr, font_size, {cx, cy}, S.col_label_text, net_id.c_str()); } } }; @@ -378,9 +412,9 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil if (is_error) { // Error: red box dl->AddRectFilled(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, - COL_NODE_ERR, 4.0f * canvas_zoom_); + S.col_node_err, S.node_rounding * canvas_zoom_); dl->AddRect(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, - IM_COL32(255, 80, 80, 255), 4.0f * canvas_zoom_); + S.col_err_border, S.node_rounding * canvas_zoom_); } // Label: no box at all @@ -389,7 +423,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil float tw = text_sz.x * canvas_zoom_; float cx = layout.pos.x + (layout.width - tw) * 0.5f; float cy = layout.pos.y + (layout.height - font_size) * 0.5f; - dl->AddText(nullptr, font_size, {cx, cy}, COL_TEXT, display.c_str()); + dl->AddText(nullptr, font_size, {cx, cy}, S.col_text, display.c_str()); } return; // no pins for special nodes } @@ -402,11 +436,11 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil bool selected = selected_nodes_.count(id); bool has_error = !node.error.empty(); - ImU32 col = has_error ? COL_NODE_ERR : (selected ? COL_NODE_SEL : COL_NODE); + ImU32 col = has_error ? S.col_node_err : (selected ? S.col_node_sel : S.col_node); dl->AddRectFilled(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, - col, 4.0f * canvas_zoom_); + col, S.node_rounding * canvas_zoom_); dl->AddRect(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, - IM_COL32(80, 80, 100, 255), 4.0f * canvas_zoom_); + S.col_node_border, S.node_rounding * canvas_zoom_); // Text float font_size = ImGui::GetFontSize() * canvas_zoom_; @@ -415,12 +449,12 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil float tw = text_sz.x * canvas_zoom_; float cx = layout.pos.x + (layout.width - tw) * 0.5f; float cy = layout.pos.y + (layout.height - font_size) * 0.5f; - dl->AddText(nullptr, font_size, {cx, cy}, COL_TEXT, display.c_str()); + dl->AddText(nullptr, font_size, {cx, cy}, S.col_text, display.c_str()); } // Draw input pins (top) // Pin order: parsed_args ArgNet2 (inputs merged first), then va_args ArgNet2, then remaps - float pr = PIN_RADIUS * canvas_zoom_; + float pr = S.pin_radius * canvas_zoom_; bool has_va = nt->va_args != nullptr; auto count_net_args_in = [](const ParsedArgs2* pa) -> int { if (!pa) return 0; @@ -436,11 +470,11 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil // Draw the "add more" diamond at the boundary between va_args and remaps if (has_va && i == add_pin_pos) { - ImU32 pc = IM_COL32(120, 120, 140, 180); + ImU32 pc = S.col_add_pin; dl->AddQuadFilled({pp.x, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x, pp.y + pr}, {pp.x - pr, pp.y}, pc); float cr = pr * 0.5f; - ImU32 tc = IM_COL32(200, 200, 220, 220); - float lth = 1.5f * canvas_zoom_; + ImU32 tc = S.col_add_pin_fg; + float lth = S.add_pin_line * canvas_zoom_; dl->AddLine({pp.x - cr, pp.y}, {pp.x + cr, pp.y}, tc, lth); dl->AddLine({pp.x, pp.y - cr}, {pp.x, pp.y + cr}, tc, lth); continue; @@ -475,7 +509,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil float scale = font_sz / ImGui::GetFontSize(); dl->AddText(nullptr, font_sz, {pp.x - ts.x * scale * 0.5f, pp.y - ts.y * scale * 0.5f}, - IM_COL32(30, 30, 40, 255), "?"); + S.col_opt_pin_fg, "?"); } } else { dl->AddCircleFilled(pp, pr, pc); @@ -500,7 +534,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil if (nt->is_flow()) { // Lambda grab handle (left-pointing triangle, middle-left) ImVec2 gp = layout.lambda_grab_pos(); - ImU32 lc = IM_COL32(180, 130, 255, 255); + ImU32 lc = S.col_pin_lambda; dl->AddTriangleFilled( {gp.x + pr, gp.y - pr}, {gp.x - pr, gp.y}, @@ -509,16 +543,16 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil // Side-bang (square, middle-right) ImVec2 bp = {layout.pos.x + layout.width, layout.pos.y + layout.height * 0.5f}; - dl->AddRectFilled({bp.x - pr, bp.y - pr}, {bp.x + pr, bp.y + pr}, COL_PIN_BANG); + dl->AddRectFilled({bp.x - pr, bp.y - pr}, {bp.x + pr, bp.y + pr}, S.col_pin_bang); } // Pin hover: highlight + tooltip ImVec2 mouse = ImGui::GetMousePos(); - float hit_r = pr * 2.5f; + float hit_r = pr * S.pin_hit_radius_mul; auto dist2 = [](ImVec2 a, ImVec2 b) { return (a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y); }; float hit_r2 = hit_r * hit_r; - float ho = 2.0f * canvas_zoom_; // highlight outline offset - ImU32 COL_HOVER = IM_COL32(255, 255, 255, 255); + float ho = S.highlight_offset * canvas_zoom_; + ImU32 COL_HOVER = S.col_pin_hover; // Helper to get input pin name auto get_input_pin_name = [&](int i) -> const char* { @@ -553,22 +587,23 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil return PinShape2::Circle; }; + float ht = S.highlight_thickness; auto draw_highlight = [&](ImVec2 pos, PinShape2 shape) { switch (shape) { case PinShape2::Circle: - dl->AddCircle(pos, pr + ho, COL_HOVER, 0, 2.0f); + dl->AddCircle(pos, pr + ho, COL_HOVER, 0, ht); break; case PinShape2::Square: - dl->AddRect({pos.x - pr - ho, pos.y - pr - ho}, {pos.x + pr + ho, pos.y + pr + ho}, COL_HOVER, 0, 0, 2.0f); + dl->AddRect({pos.x - pr - ho, pos.y - pr - ho}, {pos.x + pr + ho, pos.y + pr + ho}, COL_HOVER, 0, 0, ht); break; case PinShape2::Diamond: - dl->AddQuad({pos.x, pos.y - pr - ho}, {pos.x + pr + ho, pos.y}, {pos.x, pos.y + pr + ho}, {pos.x - pr - ho, pos.y}, COL_HOVER, 2.0f); + dl->AddQuad({pos.x, pos.y - pr - ho}, {pos.x + pr + ho, pos.y}, {pos.x, pos.y + pr + ho}, {pos.x - pr - ho, pos.y}, COL_HOVER, ht); break; case PinShape2::TriangleDown: - dl->AddTriangle({pos.x - pr - ho, pos.y - pr - ho}, {pos.x + pr + ho, pos.y - pr - ho}, {pos.x, pos.y + pr + ho}, COL_HOVER, 2.0f); + dl->AddTriangle({pos.x - pr - ho, pos.y - pr - ho}, {pos.x + pr + ho, pos.y - pr - ho}, {pos.x, pos.y + pr + ho}, COL_HOVER, ht); break; case PinShape2::TriangleLeft: - dl->AddTriangle({pos.x + pr + ho, pos.y - pr - ho}, {pos.x - pr - ho, pos.y}, {pos.x + pr + ho, pos.y + pr + ho}, COL_HOVER, 2.0f); + dl->AddTriangle({pos.x + pr + ho, pos.y - pr - ho}, {pos.x - pr - ho, pos.y}, {pos.x + pr + ho, pos.y + pr + ho}, COL_HOVER, ht); break; } }; From 3d7904a8d6d7c04e1a8b143caeaf3bcbdcd2f5d7 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 11:23:43 +0200 Subject: [PATCH 28/86] editor2: PinMapping --- src/attoflow/editor2.cpp | 196 ++++++++++++++++++++++----------------- 1 file changed, 112 insertions(+), 84 deletions(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index d2f329d..5bbd57e 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -67,6 +67,57 @@ static inline ImVec2 v2add(ImVec2 a, ImVec2 b) { return {a.x + b.x, a.y + b.y}; static inline ImVec2 v2sub(ImVec2 a, ImVec2 b) { return {a.x - b.x, a.y - b.y}; } static inline ImVec2 v2mul(ImVec2 a, float s) { return {a.x * s, a.y * s}; } +// Maps visible pin index → descriptor port index for input pins +// Sections: base_args ArgNet2, va_args ArgNet2, [+diamond], remaps +struct PinMapping { + std::vector pin_to_port; // visible pin idx → port index in parsed_args/descriptor + int base_count = 0; // visible base pins (ArgNet2 in parsed_args) + int va_count = 0; // visible va_args pins (ArgNet2 in parsed_va_args) + int add_pin_pos = -1; // position of +diamond (-1 if none) + bool has_va = false; + + static PinMapping build(const FlowNodeBuilder& node, const NodeType2* nt) { + PinMapping m; + m.has_va = nt && nt->va_args != nullptr; + // Base args: track which parsed_args indices are ArgNet2 + if (node.parsed_args) { + for (int i = 0; i < (int)node.parsed_args->size(); i++) { + if (std::holds_alternative((*node.parsed_args)[i])) { + m.pin_to_port.push_back(i); + m.base_count++; + } + } + } + // Va_args + if (node.parsed_va_args) { + for (int i = 0; i < (int)node.parsed_va_args->size(); i++) { + if (std::holds_alternative((*node.parsed_va_args)[i])) { + m.pin_to_port.push_back(-(i + 1)); // negative = va_args index (1-based) + m.va_count++; + } + } + } + // +diamond slot + if (m.has_va) { + m.add_pin_pos = (int)m.pin_to_port.size(); + m.pin_to_port.push_back(-1000); // sentinel for +diamond + } + // Remaps + for (int i = 0; i < (int)node.remaps.size(); i++) { + m.pin_to_port.push_back(-2000 - i); // sentinel for remap i + } + return m; + } + + int total() const { return (int)pin_to_port.size(); } + bool is_base(int pin) const { return pin < base_count; } + bool is_va(int pin) const { return pin >= base_count && pin < base_count + va_count; } + bool is_add_diamond(int pin) const { return pin == add_pin_pos; } + bool is_remap(int pin) const { return pin >= base_count + va_count + (has_va ? 1 : 0); } + int port_index(int pin) const { return pin_to_port[pin]; } + int remap_index(int pin) const { return -(pin_to_port[pin] + 2000); } +}; + // Computed node layout for drawing struct NodeLayout { ImVec2 pos; // top-left screen position @@ -97,17 +148,8 @@ static NodeLayout compute_node_layout(const FlowNodeBuilder& node, ImVec2 canvas ImVec2 text_sz = ImGui::CalcTextSize(display.c_str()); float text_w = text_sz.x * zoom + 16.0f * zoom; - auto count_net_args = [](const ParsedArgs2* pa) -> int { - if (!pa) return 0; - int n = 0; - for (auto& a : *pa) if (std::holds_alternative(a)) n++; - return n; - }; - bool has_va = nt && nt->va_args != nullptr; - int num_in = count_net_args(node.parsed_args.get()) - + count_net_args(node.parsed_va_args.get()) - + (has_va ? 1 : 0) // +1 for the "add more" diamond - + (int)node.remaps.size(); + auto pm = PinMapping::build(node, nt); + int num_in = pm.total(); int num_out = nt ? nt->num_outputs : 1; if (node.type_id == NodeTypeID::Expr || node.type_id == NodeTypeID::ExprBang) { int args_count = node.parsed_args ? (int)node.parsed_args->size() : 0; @@ -293,31 +335,27 @@ void Editor2Pane::draw() { } }; - // Helper: iterate ArgNet2 entries and draw wires, returns pin count used - auto draw_wires_from_args = [&](const ParsedArgs2* pa, int pin_start) -> int { - if (!pa) return 0; - int pin = pin_start; - for (auto& a : *pa) { - if (auto* an = std::get_if(&a)) { - draw_wire_to_pin(pin++, an->second, an->first); + // Draw wires using PinMapping for correct pin positions + auto dst_pm = PinMapping::build(dst_node, dst_nt); + for (int i = 0; i < dst_pm.total(); i++) { + if (dst_pm.is_add_diamond(i)) continue; + if (dst_pm.is_base(i)) { + int port = dst_pm.port_index(i); + if (dst_node.parsed_args && port < (int)dst_node.parsed_args->size()) { + if (auto* an = std::get_if(&(*dst_node.parsed_args)[port])) + draw_wire_to_pin(i, an->second, an->first); } + } else if (dst_pm.is_va(i)) { + int va_idx = -(dst_pm.port_index(i) + 1); + if (dst_node.parsed_va_args && va_idx < (int)dst_node.parsed_va_args->size()) { + if (auto* an = std::get_if(&(*dst_node.parsed_va_args)[va_idx])) + draw_wire_to_pin(i, an->second, an->first); + } + } else if (dst_pm.is_remap(i)) { + int ri = dst_pm.remap_index(i); + if (ri < (int)dst_node.remaps.size()) + draw_wire_to_pin(i, dst_node.remaps[ri].second, dst_node.remaps[ri].first); } - return pin - pin_start; - }; - - // Base args → va_args → remaps - int pin = 0; - pin += draw_wires_from_args(dst_node.parsed_args.get(), pin); - pin += draw_wires_from_args(dst_node.parsed_va_args.get(), pin); - - // Skip +diamond slot if node has va_args - auto* dst_nt2 = find_node_type2(dst_node.type_id); - if (dst_nt2 && dst_nt2->va_args) pin++; - - // Remaps: $N pins (appended after base + va_args + add-diamond) - for (int i = 0; i < (int)dst_node.remaps.size(); i++) { - auto& remap = dst_node.remaps[i]; - draw_wire_to_pin(pin + i, remap.second, remap.first); } } @@ -452,45 +490,37 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil dl->AddText(nullptr, font_size, {cx, cy}, S.col_text, display.c_str()); } - // Draw input pins (top) - // Pin order: parsed_args ArgNet2 (inputs merged first), then va_args ArgNet2, then remaps + // Draw input pins (top) using PinMapping for correct port-to-pin association float pr = S.pin_radius * canvas_zoom_; - bool has_va = nt->va_args != nullptr; - auto count_net_args_in = [](const ParsedArgs2* pa) -> int { - if (!pa) return 0; - int n = 0; - for (auto& a : *pa) if (std::holds_alternative(a)) n++; - return n; - }; - int base_pin_count = count_net_args_in(node.parsed_args.get()); - int va_pin_count = count_net_args_in(node.parsed_va_args.get()); - int add_pin_pos = base_pin_count + va_pin_count; // where the +diamond goes (if va_args) + auto pm = PinMapping::build(node, nt); for (int i = 0; i < layout.num_in; i++) { ImVec2 pp = layout.input_pin_pos(i); - // Draw the "add more" diamond at the boundary between va_args and remaps - if (has_va && i == add_pin_pos) { + // +diamond slot + if (pm.is_add_diamond(i)) { ImU32 pc = S.col_add_pin; dl->AddQuadFilled({pp.x, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x, pp.y + pr}, {pp.x - pr, pp.y}, pc); float cr = pr * 0.5f; - ImU32 tc = S.col_add_pin_fg; float lth = S.add_pin_line * canvas_zoom_; - dl->AddLine({pp.x - cr, pp.y}, {pp.x + cr, pp.y}, tc, lth); - dl->AddLine({pp.x, pp.y - cr}, {pp.x, pp.y + cr}, tc, lth); + dl->AddLine({pp.x - cr, pp.y}, {pp.x + cr, pp.y}, S.col_add_pin_fg, lth); + dl->AddLine({pp.x, pp.y - cr}, {pp.x, pp.y + cr}, S.col_add_pin_fg, lth); continue; } PortKind2 kind = PortKind2::Data; - bool is_va = false; + bool is_va = pm.is_va(i); bool is_optional = false; - if (i < base_pin_count && nt->input_ports && i < nt->num_inputs) { - kind = nt->input_ports[i].kind; - is_optional = nt->input_ports[i].optional; - } else if (i >= base_pin_count && i < add_pin_pos) { + + if (pm.is_base(i)) { + int port = pm.port_index(i); + if (nt->input_ports && port < nt->num_inputs) { + kind = nt->input_ports[port].kind; + is_optional = nt->input_ports[port].optional; + } + } else if (is_va) { kind = nt->va_args ? nt->va_args->kind : PortKind2::Data; - is_va = true; } - // pins after add_pin_pos are remaps (Data) + // remaps are always Data ImU32 pc = pin_color(kind); if (kind == PortKind2::BangTrigger) { @@ -498,10 +528,8 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil } else if (kind == PortKind2::Lambda) { dl->AddTriangleFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y - pr}, {pp.x, pp.y + pr}, pc); } else if (is_va) { - // Diamond for va_args pins dl->AddQuadFilled({pp.x, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x, pp.y + pr}, {pp.x - pr, pp.y}, pc); } else if (is_optional) { - // Diamond with ? for optional pins dl->AddQuadFilled({pp.x, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x, pp.y + pr}, {pp.x - pr, pp.y}, pc); float font_sz = pr * 1.6f; if (font_sz > 3.0f) { @@ -554,36 +582,36 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil float ho = S.highlight_offset * canvas_zoom_; ImU32 COL_HOVER = S.col_pin_hover; - // Helper to get input pin name + // Helper to get input pin name using PinMapping auto get_input_pin_name = [&](int i) -> const char* { - int adj = (has_va && i > add_pin_pos) ? i - 1 : i; - if (adj < base_pin_count && nt->input_ports && adj < nt->num_inputs) - return nt->input_ports[adj].name; - if (adj >= base_pin_count && adj < add_pin_pos && nt->va_args) - return nt->va_args->name; - int remap_idx = adj - add_pin_pos; - static char remap_buf[16]; - snprintf(remap_buf, sizeof(remap_buf), "$%d", remap_idx); - return remap_buf; + if (pm.is_base(i)) { + int port = pm.port_index(i); + if (nt->input_ports && port < nt->num_inputs) + return nt->input_ports[port].name; + } else if (pm.is_va(i)) { + return nt->va_args ? nt->va_args->name : "va"; + } else if (pm.is_remap(i)) { + static char remap_buf[16]; + snprintf(remap_buf, sizeof(remap_buf), "$%d", pm.remap_index(i)); + return remap_buf; + } + return "?"; }; // Helper to get input pin shape for highlight enum class PinShape2 { Circle, Square, Diamond, TriangleDown, TriangleLeft }; auto get_input_pin_shape = [&](int i) -> PinShape2 { - if (has_va && i == add_pin_pos) return PinShape2::Diamond; - int adj = (has_va && i > add_pin_pos) ? i - 1 : i; - PortKind2 kind = PortKind2::Data; - bool is_va_pin = false; - bool is_opt = false; - if (adj < base_pin_count && nt->input_ports && adj < nt->num_inputs) { - kind = nt->input_ports[adj].kind; - is_opt = nt->input_ports[adj].optional; - } else if (adj >= base_pin_count && adj < add_pin_pos) { - is_va_pin = true; + if (pm.is_add_diamond(i)) return PinShape2::Diamond; + if (pm.is_base(i)) { + int port = pm.port_index(i); + if (nt->input_ports && port < nt->num_inputs) { + if (nt->input_ports[port].kind == PortKind2::BangTrigger) return PinShape2::Square; + if (nt->input_ports[port].kind == PortKind2::Lambda) return PinShape2::TriangleDown; + if (nt->input_ports[port].optional) return PinShape2::Diamond; + } + } else if (pm.is_va(i)) { + return PinShape2::Diamond; } - if (kind == PortKind2::BangTrigger) return PinShape2::Square; - if (kind == PortKind2::Lambda) return PinShape2::TriangleDown; - if (is_va_pin || is_opt) return PinShape2::Diamond; return PinShape2::Circle; }; @@ -611,7 +639,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil bool pin_hovered = false; // Check input pins for (int i = 0; i < layout.num_in; i++) { - if (has_va && i == add_pin_pos) continue; + if (pm.is_add_diamond(i)) continue; ImVec2 pp = layout.input_pin_pos(i); if (dist2(mouse, pp) < hit_r2) { draw_highlight(pp, get_input_pin_shape(i)); From 0522f0f25ca65ec8f8afbb6aa366f040ca3fd793 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 11:31:48 +0200 Subject: [PATCH 29/86] optionals are indeed optionals now --- src/atto/graphbuilder.cpp | 15 +++++++++++++++ src/attoflow/editor2.cpp | 32 ++++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index 95d0fa8..04f62d9 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -675,6 +675,21 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { nb.remaps.push_back({"$unconnected", unconnected}); } + // Trim trailing $unconnected optional ports from parsed_args + { + auto* trim_nt = find_node_type2(nb.type_id); + if (trim_nt && nb.parsed_args) { + while (!nb.parsed_args->empty()) { + int idx = (int)nb.parsed_args->size() - 1; + if (idx >= trim_nt->num_inputs) break; + if (!trim_nt->input_ports[idx].optional) break; + auto* an = std::get_if(&nb.parsed_args->back()); + if (!an || an->first != "$unconnected") break; + nb.parsed_args->pop_back(); + } + } + } + cur_id.clear(); cur_type.clear(); cur_args.clear(); cur_inputs.clear(); cur_outputs.clear(); cur_x = 0; cur_y = 0; cur_shadow = false; diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 5bbd57e..3d653b3 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -71,7 +71,7 @@ static inline ImVec2 v2mul(ImVec2 a, float s) { return {a.x * s, a.y * s}; } // Sections: base_args ArgNet2, va_args ArgNet2, [+diamond], remaps struct PinMapping { std::vector pin_to_port; // visible pin idx → port index in parsed_args/descriptor - int base_count = 0; // visible base pins (ArgNet2 in parsed_args) + int base_count = 0; // visible base pins (ArgNet2 in parsed_args + absent optionals) int va_count = 0; // visible va_args pins (ArgNet2 in parsed_va_args) int add_pin_pos = -1; // position of +diamond (-1 if none) bool has_va = false; @@ -79,15 +79,26 @@ struct PinMapping { static PinMapping build(const FlowNodeBuilder& node, const NodeType2* nt) { PinMapping m; m.has_va = nt && nt->va_args != nullptr; + int parsed_size = node.parsed_args ? (int)node.parsed_args->size() : 0; + // Base args: track which parsed_args indices are ArgNet2 if (node.parsed_args) { - for (int i = 0; i < (int)node.parsed_args->size(); i++) { + for (int i = 0; i < parsed_size; i++) { if (std::holds_alternative((*node.parsed_args)[i])) { m.pin_to_port.push_back(i); m.base_count++; } } } + // Absent trailing optional ports: show as pins beyond parsed_args + if (nt) { + for (int i = parsed_size; i < nt->num_inputs; i++) { + if (nt->input_ports[i].optional) { + m.pin_to_port.push_back(-3000 - i); // sentinel for absent optional at port i + m.base_count++; + } + } + } // Va_args if (node.parsed_va_args) { for (int i = 0; i < (int)node.parsed_va_args->size(); i++) { @@ -111,6 +122,8 @@ struct PinMapping { int total() const { return (int)pin_to_port.size(); } bool is_base(int pin) const { return pin < base_count; } + bool is_absent_optional(int pin) const { return pin < base_count && pin_to_port[pin] <= -3000; } + int absent_port_index(int pin) const { return -(pin_to_port[pin] + 3000); } bool is_va(int pin) const { return pin >= base_count && pin < base_count + va_count; } bool is_add_diamond(int pin) const { return pin == add_pin_pos; } bool is_remap(int pin) const { return pin >= base_count + va_count + (has_va ? 1 : 0); } @@ -339,6 +352,7 @@ void Editor2Pane::draw() { auto dst_pm = PinMapping::build(dst_node, dst_nt); for (int i = 0; i < dst_pm.total(); i++) { if (dst_pm.is_add_diamond(i)) continue; + if (dst_pm.is_absent_optional(i)) continue; if (dst_pm.is_base(i)) { int port = dst_pm.port_index(i); if (dst_node.parsed_args && port < (int)dst_node.parsed_args->size()) { @@ -511,7 +525,12 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil bool is_va = pm.is_va(i); bool is_optional = false; - if (pm.is_base(i)) { + if (pm.is_absent_optional(i)) { + int port = pm.absent_port_index(i); + if (nt->input_ports && port < nt->num_inputs) + kind = nt->input_ports[port].kind; + is_optional = true; + } else if (pm.is_base(i)) { int port = pm.port_index(i); if (nt->input_ports && port < nt->num_inputs) { kind = nt->input_ports[port].kind; @@ -584,7 +603,11 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil // Helper to get input pin name using PinMapping auto get_input_pin_name = [&](int i) -> const char* { - if (pm.is_base(i)) { + if (pm.is_absent_optional(i)) { + int port = pm.absent_port_index(i); + if (nt->input_ports && port < nt->num_inputs) + return nt->input_ports[port].name; + } else if (pm.is_base(i)) { int port = pm.port_index(i); if (nt->input_ports && port < nt->num_inputs) return nt->input_ports[port].name; @@ -602,6 +625,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil enum class PinShape2 { Circle, Square, Diamond, TriangleDown, TriangleLeft }; auto get_input_pin_shape = [&](int i) -> PinShape2 { if (pm.is_add_diamond(i)) return PinShape2::Diamond; + if (pm.is_absent_optional(i)) return PinShape2::Diamond; if (pm.is_base(i)) { int port = pm.port_index(i); if (nt->input_ports && port < nt->num_inputs) { From 1768a5cf93967baf9df36e531f872aa1e6414acd Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 11:44:14 +0200 Subject: [PATCH 30/86] Split input_ports_optional --- src/atto/graphbuilder.cpp | 20 +- src/atto/node_types2.h | 699 ++++++++++++++++++++++++-------------- src/attoflow/editor2.cpp | 29 +- 3 files changed, 474 insertions(+), 274 deletions(-) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index 04f62d9..0c878c3 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -622,9 +622,9 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { }; // Pass 1: fill by name matching - std::vector filled(new_nt->num_inputs, false); - for (int i = 0; i < new_nt->num_inputs; i++) { - auto [found, value] = find_by_name(new_nt->input_ports[i].name); + std::vector filled(new_nt->total_inputs(), false); + for (int i = 0; i < new_nt->total_inputs(); i++) { + auto [found, value] = find_by_name(new_nt->input_port(i)->name); if (found) { merged->push_back(std::move(value)); filled[i] = true; @@ -643,8 +643,8 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { consumed = std::min(consumed, old_nt->inputs); } int arg_cursor = consumed; - for (int i = 0; i < new_nt->num_inputs; i++) { - if (!filled[i] && new_nt->input_ports[i].kind != PortKind2::BangTrigger) { + for (int i = 0; i < new_nt->total_inputs(); i++) { + if (!filled[i] && new_nt->input_port(i)->kind != PortKind2::BangTrigger) { if (arg_cursor < (int)nb.parsed_args->size()) { (*merged)[i] = std::move((*nb.parsed_args)[arg_cursor++]); filled[i] = true; @@ -676,13 +676,11 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { } // Trim trailing $unconnected optional ports from parsed_args + // Optional ports are always trailing: anything beyond num_inputs is optional { auto* trim_nt = find_node_type2(nb.type_id); if (trim_nt && nb.parsed_args) { - while (!nb.parsed_args->empty()) { - int idx = (int)nb.parsed_args->size() - 1; - if (idx >= trim_nt->num_inputs) break; - if (!trim_nt->input_ports[idx].optional) break; + while ((int)nb.parsed_args->size() > trim_nt->num_inputs) { auto* an = std::get_if(&nb.parsed_args->back()); if (!an || an->first != "$unconnected") break; nb.parsed_args->pop_back(); @@ -858,8 +856,8 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { auto* nt = find_node_type2(node.type_id); if (!nt || !nt->va_args || !node.parsed_args) continue; - // Split at descriptor input count — inputs are merged first, then args - int fixed_args = nt->num_inputs; + // Split at total descriptor input count (required + optional) + int fixed_args = nt->total_inputs(); if ((int)node.parsed_args->size() > fixed_args) { node.parsed_va_args = std::make_shared(); diff --git a/src/atto/node_types2.h b/src/atto/node_types2.h index 2b1e43e..29b2186 100644 --- a/src/atto/node_types2.h +++ b/src/atto/node_types2.h @@ -30,13 +30,22 @@ struct NodeType2 { NodeTypeID type_id; const char* name; const char* desc; - const PortDesc2* input_ports; - int num_inputs; + const PortDesc2* input_ports = nullptr; + int num_inputs = 0; // required input ports + const PortDesc2* input_optional_ports = nullptr; + int num_inputs_optional = 0; // trailing optional input ports const PortDesc2* output_ports; int num_outputs; NodeKind2 kind = NodeKind2::Flow; const PortDesc2* va_args = nullptr; // nullptr = no va_args, else template for repeating pins + int total_inputs() const { return num_inputs + num_inputs_optional; } + const PortDesc2* input_port(int i) const { + if (i < num_inputs) return input_ports ? &input_ports[i] : nullptr; + int oi = i - num_inputs; + if (oi < num_inputs_optional) return input_optional_ports ? &input_optional_ports[oi] : nullptr; + return nullptr; + } bool is_banged() const { return kind == NodeKind2::Banged || kind == NodeKind2::Event || kind == NodeKind2::Declaration; } bool is_declaration() const { return kind == NodeKind2::Declaration; } bool is_flow() const { return kind == NodeKind2::Flow; } @@ -47,328 +56,524 @@ struct NodeType2 { // ─── Port descriptor arrays ─── // Common outputs -static const PortDesc2 P2_NEXT[] = {{"next", "fires after completion", PortKind2::BangNext}}; -static const PortDesc2 P2_RESULT[] = {{"result", "result value"}}; -static const PortDesc2 P2_NEXT_RESULT[] = {{"next", "fires after completion", PortKind2::BangNext}, {"result", "result value"}}; +static const PortDesc2 P2_NEXT[] = { + {.name = "next", .desc = "fires after completion", .kind = PortKind2::BangNext}, +}; +static const PortDesc2 P2_RESULT[] = { + {.name = "result", .desc = "result value"}, +}; +static const PortDesc2 P2_NEXT_RESULT[] = { + {.name = "next", .desc = "fires after completion", .kind = PortKind2::BangNext}, + {.name = "result", .desc = "result value"}, +}; // Common inputs -static const PortDesc2 P2_BANG_IN[] = {{"bang_in", "trigger input", PortKind2::BangTrigger}}; -static const PortDesc2 P2_VALUE[] = {{"value", "input value"}}; +static const PortDesc2 P2_BANG_IN[] = { + {.name = "bang_in", .desc = "trigger input", .kind = PortKind2::BangTrigger}, +}; +static const PortDesc2 P2_VALUE[] = { + {.name = "value", .desc = "input value"}, +}; -// expr! inputs -static const PortDesc2 P2_EXPR_BANG_IN[] = {{"bang_in", "trigger input", PortKind2::BangTrigger}}; +// expr! +static const PortDesc2 P2_EXPR_BANG_IN[] = { + {.name = "bang_in", .desc = "trigger input", .kind = PortKind2::BangTrigger}, +}; -// store! inputs: bang, target, value +// store! static const PortDesc2 P2_STORE_BANG_IN[] = { - {"bang_in", "trigger", PortKind2::BangTrigger}, - {"target", "variable/reference to store into"}, - {"value", "value to store"}, + {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, + {.name = "target", .desc = "variable/reference to store into"}, + {.name = "value", .desc = "value to store"}, }; - -// store (no bang) inputs: target, value static const PortDesc2 P2_STORE_IN[] = { - {"target", "variable/reference to store into"}, - {"value", "value to store"}, + {.name = "target", .desc = "variable/reference to store into"}, + {.name = "value", .desc = "value to store"}, }; -// append! inputs: bang, target, value +// append! static const PortDesc2 P2_APPEND_BANG_IN[] = { - {"bang_in", "trigger", PortKind2::BangTrigger}, - {"target", "collection to append to"}, - {"value", "value to append"}, + {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, + {.name = "target", .desc = "collection to append to"}, + {.name = "value", .desc = "value to append"}, }; - -// append (no bang) inputs: target, value static const PortDesc2 P2_APPEND_IN[] = { - {"target", "collection to append to"}, - {"value", "value to append"}, + {.name = "target", .desc = "collection to append to"}, + {.name = "value", .desc = "value to append"}, }; -// erase inputs +// erase static const PortDesc2 P2_ERASE_BANG_IN[] = { - {"bang_in", "trigger", PortKind2::BangTrigger}, - {"target", "collection to erase from"}, - {"key", "key/value/iterator to erase"}, + {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, + {.name = "target", .desc = "collection to erase from"}, + {.name = "key", .desc = "key/value/iterator to erase"}, }; static const PortDesc2 P2_ERASE_IN[] = { - {"target", "collection to erase from"}, - {"key", "key/value/iterator to erase"}, + {.name = "target", .desc = "collection to erase from"}, + {.name = "key", .desc = "key/value/iterator to erase"}, }; -// select inputs: condition, if_true, if_false +// select static const PortDesc2 P2_SELECT_IN[] = { - {"condition", "boolean selector"}, - {"if_true", "value when true"}, - {"if_false", "value when false"}, + {.name = "condition", .desc = "boolean selector"}, + {.name = "if_true", .desc = "value when true"}, + {.name = "if_false", .desc = "value when false"}, }; - -// select! inputs: bang, condition static const PortDesc2 P2_SELECT_BANG_IN[] = { - {"bang_in", "trigger", PortKind2::BangTrigger}, - {"condition", "boolean condition"}, + {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, + {.name = "condition", .desc = "boolean condition"}, }; -// select! outputs: next, true, false static const PortDesc2 P2_SELECT_BANG_OUT[] = { - {"next", "fires after branch completes", PortKind2::BangNext}, - {"true", "fires when true", PortKind2::BangNext}, - {"false", "fires when false", PortKind2::BangNext}, + {.name = "next", .desc = "fires after branch completes", .kind = PortKind2::BangNext}, + {.name = "true", .desc = "fires when true", .kind = PortKind2::BangNext}, + {.name = "false", .desc = "fires when false", .kind = PortKind2::BangNext}, }; // va_args templates -static const PortDesc2 P2_VA_FIELD = {"field", "constructor field"}; -static const PortDesc2 P2_VA_ARG = {"arg", "function argument"}; -static const PortDesc2 P2_VA_PARAM = {"param", "lambda parameter"}; +static const PortDesc2 P2_VA_FIELD = {.name = "field", .desc = "constructor field"}; +static const PortDesc2 P2_VA_ARG = {.name = "arg", .desc = "function argument"}; +static const PortDesc2 P2_VA_PARAM = {.name = "param", .desc = "lambda parameter"}; -// new: type as fixed input, va_args fields +// new static const PortDesc2 P2_NEW_IN[] = { - {"type", "type to instantiate"}, + {.name = "type", .desc = "type to instantiate"}, }; -// call: function ref as fixed input, va_args arguments +// call static const PortDesc2 P2_CALL_IN[] = { - {"fn", "function to call"}, + {.name = "fn", .desc = "function to call"}, }; -// call!: bang + function ref, va_args arguments static const PortDesc2 P2_CALL_BANG_IN[] = { - {"bang_in", "trigger", PortKind2::BangTrigger}, - {"fn", "function to call"}, + {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, + {.name = "fn", .desc = "function to call"}, }; -// iterate: collection + fn(lambda) +// iterate static const PortDesc2 P2_ITERATE_IN[] = { - {"collection", "collection to iterate over"}, - {"fn", "it=fn(it); while it!=end", PortKind2::Lambda}, + {.name = "collection", .desc = "collection to iterate over"}, + {.name = "fn", .desc = "it=fn(it); while it!=end", .kind = PortKind2::Lambda}, }; static const PortDesc2 P2_ITERATE_BANG_IN[] = { - {"bang_in", "trigger", PortKind2::BangTrigger}, - {"collection", "collection to iterate over"}, - {"fn", "it=fn(it); while it!=end", PortKind2::Lambda}, + {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, + {.name = "collection", .desc = "collection to iterate over"}, + {.name = "fn", .desc = "it=fn(it); while it!=end", .kind = PortKind2::Lambda}, }; -// lock: mutex + fn(lambda), va_args handled by NodeType2::va_args +// lock static const PortDesc2 P2_LOCK_IN[] = { - {"mutex", "mutex to lock"}, - {"fn", "body under lock", PortKind2::Lambda}, + {.name = "mutex", .desc = "mutex to lock"}, + {.name = "fn", .desc = "body under lock", .kind = PortKind2::Lambda}, }; static const PortDesc2 P2_LOCK_BANG_IN[] = { - {"bang_in", "trigger", PortKind2::BangTrigger}, - {"mutex", "mutex to lock"}, - {"fn", "body under lock", PortKind2::Lambda}, + {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, + {.name = "mutex", .desc = "mutex to lock"}, + {.name = "fn", .desc = "body under lock", .kind = PortKind2::Lambda}, }; -// decl inputs +// decl static const PortDesc2 P2_DECL_TYPE_IN[] = { - {"bang_in", "trigger", PortKind2::BangTrigger}, - {"name", "type name (symbol)"}, - {"type", "type definition"}, + {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, + {.name = "name", .desc = "type name (symbol)"}, + {.name = "type", .desc = "type definition"}, }; static const PortDesc2 P2_DECL_TYPE_OUT[] = { - {"next", "fires after declaration", PortKind2::BangNext}, - {"type", "the declared type"}, + {.name = "next", .desc = "fires after declaration", .kind = PortKind2::BangNext}, + {.name = "type", .desc = "the declared type"}, }; static const PortDesc2 P2_DECL_VAR_IN[] = { - {"bang_in", "trigger", PortKind2::BangTrigger}, - {"name", "variable name (symbol)"}, - {"type", "variable type"}, - {"initial", "variable initial value", PortKind2::Data, nullptr, true}, + {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, + {.name = "name", .desc = "variable name (symbol)"}, + {.name = "type", .desc = "variable type"}, +}; +static const PortDesc2 P2_DECL_VAR_OPT_IN[] = { + {.name = "initial", .desc = "variable initial value", .optional = true}, }; static const PortDesc2 P2_DECL_VAR_OUT[] = { - {"next", "fires after declaration", PortKind2::BangNext}, - {"ref", "reference to variable"}, + {.name = "next", .desc = "fires after declaration", .kind = PortKind2::BangNext}, + {.name = "ref", .desc = "reference to variable"}, +}; +static const PortDesc2 P2_DECL_OUT[] = { + {.name = "next", .desc = "fires to start declarations", .kind = PortKind2::BangNext}, }; -static const PortDesc2 P2_DECL_OUT[] = {{"next", "fires to start declarations", PortKind2::BangNext}}; static const PortDesc2 P2_DECL_EVENT_IN[] = { - {"bang_in", "trigger", PortKind2::BangTrigger}, - {"name", "event name (symbol)"}, - {"type", "event function type"}, + {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, + {.name = "name", .desc = "event name (symbol)"}, + {.name = "type", .desc = "event function type"}, }; static const PortDesc2 P2_DECL_IMPORT_IN[] = { - {"bang_in", "trigger", PortKind2::BangTrigger}, - {"path", "module path", PortKind2::Data, "literal"}, + {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, + {.name = "path", .desc = "module path", .type_name = "literal"}, }; static const PortDesc2 P2_FFI_IN[] = { - {"bang_in", "trigger", PortKind2::BangTrigger}, - {"name", "function name (symbol)"}, - {"type", "function type"}, + {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, + {.name = "name", .desc = "function name (symbol)"}, + {.name = "type", .desc = "function type"}, }; // discard static const PortDesc2 P2_DISCARD_BANG_IN[] = { - {"bang_in", "trigger", PortKind2::BangTrigger}, - {"value", "value to discard"}, + {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, + {.name = "value", .desc = "value to discard"}, }; // output_mix! static const PortDesc2 P2_OUTPUT_MIX_IN[] = { - {"bang_in", "trigger", PortKind2::BangTrigger}, - {"value", "audio sample to mix"}, + {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, + {.name = "value", .desc = "audio sample to mix"}, }; // resize! static const PortDesc2 P2_RESIZE_IN[] = { - {"bang_in", "trigger", PortKind2::BangTrigger}, - {"target", "vector to resize"}, - {"size", "new size", PortKind2::Data, "s32"}, + {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, + {.name = "target", .desc = "vector to resize"}, + {.name = "size", .desc = "new size", .type_name = "s32"}, }; - -// event! outputs -static const PortDesc2 P2_EVENT_OUT[] = {{"next", "fires on event", PortKind2::BangNext}}; +// event! +static const PortDesc2 P2_EVENT_OUT[] = { + {.name = "next", .desc = "fires on event", .kind = PortKind2::BangNext}, +}; // ─── Node type table ─── static const NodeType2 NODE_TYPES2[] = { - // expr: no fixed inputs, outputs = args count - {NodeTypeID::Expr, "expr", "Evaluate expression", - nullptr, 0, P2_RESULT, 1}, - - // select: 3 fixed inputs, 1 output - {NodeTypeID::Select, "select", "Select value by condition", - P2_SELECT_IN, 3, P2_RESULT, 1}, - - // new: type fixed input + va_args fields, 1 output - {NodeTypeID::New, "new", "Instantiate a type", - P2_NEW_IN, 1, P2_RESULT, 1, NodeKind2::Flow, &P2_VA_FIELD}, - - // dup: 1 input, 1 output - {NodeTypeID::Dup, "dup", "Duplicate input to output", - P2_VALUE, 1, P2_RESULT, 1}, - - // str: 1 input, 1 output - {NodeTypeID::Str, "str", "Convert to string", - P2_VALUE, 1, P2_RESULT, 1}, - - // void: no inputs, 1 output - {NodeTypeID::Void, "void", "Void result", - nullptr, 0, P2_RESULT, 1}, - - // discard!: bang + value, next output - {NodeTypeID::DiscardBang, "discard!", "Discard value, pass bang", - P2_DISCARD_BANG_IN, 2, P2_NEXT, 1, NodeKind2::Banged}, - - // discard: 1 input, no outputs - {NodeTypeID::Discard, "discard", "Discard input values", - P2_VALUE, 1, nullptr, 0}, - - // decl_type - {NodeTypeID::DeclType, "decl_type", "Declare a type", - P2_DECL_TYPE_IN, 3, P2_DECL_TYPE_OUT, 2, NodeKind2::Declaration}, - - // decl_var - {NodeTypeID::DeclVar, "decl_var", "Declare a variable", - P2_DECL_VAR_IN, 4, P2_DECL_VAR_OUT, 2, NodeKind2::Declaration}, - - // decl - {NodeTypeID::Decl, "decl", "Compile-time entry point", - nullptr, 0, P2_DECL_OUT, 1, NodeKind2::Declaration}, - - // decl_event - {NodeTypeID::DeclEvent, "decl_event", "Declare event", - P2_DECL_EVENT_IN, 3, P2_NEXT, 1, NodeKind2::Declaration}, - - // decl_import - {NodeTypeID::DeclImport, "decl_import","Import module", - P2_DECL_IMPORT_IN, 2, P2_NEXT, 1, NodeKind2::Declaration}, - - // ffi - {NodeTypeID::Ffi, "ffi", "Declare external function", - P2_FFI_IN, 3, P2_NEXT, 1, NodeKind2::Declaration}, - - // call: fn fixed input + va_args, 1 output - {NodeTypeID::Call, "call", "Call function", - P2_CALL_IN, 1, P2_RESULT, 1, NodeKind2::Flow, &P2_VA_ARG}, - - // call!: bang + fn fixed input + va_args, next + result - {NodeTypeID::CallBang, "call!", "Call function (bang)", - P2_CALL_BANG_IN, 2, P2_NEXT_RESULT, 2, NodeKind2::Banged, &P2_VA_ARG}, - - // erase: 2 inputs, 1 output - {NodeTypeID::Erase, "erase", "Erase from collection", - P2_ERASE_IN, 2, P2_RESULT, 1}, - - // output_mix! - {NodeTypeID::OutputMixBang, "output_mix!","Mix into audio output", - P2_OUTPUT_MIX_IN, 2, nullptr, 0, NodeKind2::Banged}, - - // append: 2 inputs, 1 output - {NodeTypeID::Append, "append", "Append to collection", - P2_APPEND_IN, 2, P2_RESULT, 1}, - - // append!: bang + 2 inputs, next + result - {NodeTypeID::AppendBang, "append!", "Append to collection (bang)", - P2_APPEND_BANG_IN, 3, P2_NEXT_RESULT, 2, NodeKind2::Banged}, - - // store: 2 inputs, no outputs - {NodeTypeID::Store, "store", "Store value", - P2_STORE_IN, 2, nullptr, 0}, - - // store!: bang + 2 inputs, next - {NodeTypeID::StoreBang, "store!", "Store value (bang)", - P2_STORE_BANG_IN, 3, P2_NEXT, 1, NodeKind2::Banged}, - - // event!: no inputs, next output - {NodeTypeID::EventBang, "event!", "Event source", - nullptr, 0, P2_EVENT_OUT, 1, NodeKind2::Event}, - - // on_key_down! — removed - {NodeTypeID::OnKeyDownBang, "on_key_down!","(removed)", - nullptr, 0, nullptr, 0}, - - // on_key_up! — removed - {NodeTypeID::OnKeyUpBang, "on_key_up!", "(removed)", - nullptr, 0, nullptr, 0}, - - // select!: bang + condition, 3 bang outputs - {NodeTypeID::SelectBang, "select!", "Branch on condition", - P2_SELECT_BANG_IN, 2, P2_SELECT_BANG_OUT, 3, NodeKind2::Banged}, - - // expr!: bang input, next + outputs (dynamic) - {NodeTypeID::ExprBang, "expr!", "Evaluate expression on bang", - P2_EXPR_BANG_IN, 1, P2_NEXT, 1, NodeKind2::Banged}, - - // erase!: bang + 2 inputs, next + result - {NodeTypeID::EraseBang, "erase!", "Erase from collection (bang)", - P2_ERASE_BANG_IN, 3, P2_NEXT_RESULT, 2, NodeKind2::Banged}, - - // iterate: collection + fn, no outputs - {NodeTypeID::Iterate, "iterate", "Iterate collection", - P2_ITERATE_IN, 2, nullptr, 0}, - - // iterate!: bang + collection + fn, next - {NodeTypeID::IterateBang, "iterate!", "Iterate collection (bang)", - P2_ITERATE_BANG_IN, 3, P2_NEXT, 1, NodeKind2::Banged}, - - // next: 1 input, 1 output - {NodeTypeID::Next, "next", "Advance iterator", - P2_VALUE, 1, P2_RESULT, 1}, - - // lock: mutex + fn fixed inputs + va_args params, no outputs - {NodeTypeID::Lock, "lock", "Execute under mutex lock", - P2_LOCK_IN, 2, nullptr, 0, NodeKind2::Flow, &P2_VA_PARAM}, - - // lock!: bang + mutex + fn fixed inputs + va_args params, next - {NodeTypeID::LockBang, "lock!", "Execute under mutex lock (bang)", - P2_LOCK_BANG_IN, 3, P2_NEXT, 1, NodeKind2::Banged, &P2_VA_PARAM}, - - // resize!: bang + target + size, next - {NodeTypeID::ResizeBang, "resize!", "Resize vector", - P2_RESIZE_IN, 3, P2_NEXT, 1, NodeKind2::Banged}, - - // cast: 1 input, 1 output - {NodeTypeID::Cast, "cast", "Cast value to type", - P2_VALUE, 1, P2_RESULT, 1}, - - // label: no pins - {NodeTypeID::Label, "label", "Text label", - nullptr, 0, nullptr, 0, NodeKind2::Special}, - - // deref: 1 input, 1 output - {NodeTypeID::Deref, "deref", "Dereference iterator (internal)", - P2_VALUE, 1, P2_RESULT, 1}, - - // error: no pins - {NodeTypeID::Error, "error", "Error: invalid node", - nullptr, 0, nullptr, 0, NodeKind2::Special}, + { + .type_id = NodeTypeID::Expr, + .name = "expr", + .desc = "Evaluate expression", + .output_ports = P2_RESULT, + .num_outputs = 1 + }, + { + .type_id = NodeTypeID::Select, + .name = "select", + .desc = "Select value by condition", + .input_ports = P2_SELECT_IN, + .num_inputs = 3, + .output_ports = P2_RESULT, + .num_outputs = 1 + }, + { + .type_id = NodeTypeID::New, + .name = "new", + .desc = "Instantiate a type", + .input_ports = P2_NEW_IN, + .num_inputs = 1, + .output_ports = P2_RESULT, + .num_outputs = 1, + .va_args = &P2_VA_FIELD + }, + { + .type_id = NodeTypeID::Dup, + .name = "dup", + .desc = "Duplicate input to output", + .input_ports = P2_VALUE, + .num_inputs = 1, + .output_ports = P2_RESULT, + .num_outputs = 1 + }, + { + .type_id = NodeTypeID::Str, + .name = "str", + .desc = "Convert to string", + .input_ports = P2_VALUE, + .num_inputs = 1, + .output_ports = P2_RESULT, + .num_outputs = 1 + }, + { + .type_id = NodeTypeID::Void, + .name = "void", + .desc = "Void result", + .output_ports = P2_RESULT, + .num_outputs = 1 + }, + { + .type_id = NodeTypeID::DiscardBang, + .name = "discard!", + .desc = "Discard value, pass bang", + .input_ports = P2_DISCARD_BANG_IN, + .num_inputs = 2, + .output_ports = P2_NEXT, + .num_outputs = 1, + .kind = NodeKind2::Banged + }, + { + .type_id = NodeTypeID::Discard, + .name = "discard", + .desc = "Discard input values", + .input_ports = P2_VALUE, + .num_inputs = 1 + }, + { + .type_id = NodeTypeID::DeclType, + .name = "decl_type", + .desc = "Declare a type", + .input_ports = P2_DECL_TYPE_IN, + .num_inputs = 3, + .output_ports = P2_DECL_TYPE_OUT, + .num_outputs = 2, + .kind = NodeKind2::Declaration + }, + { + .type_id = NodeTypeID::DeclVar, + .name = "decl_var", + .desc = "Declare a variable", + .input_ports = P2_DECL_VAR_IN, + .num_inputs = 3, + .input_optional_ports = P2_DECL_VAR_OPT_IN, + .num_inputs_optional = 1, + .output_ports = P2_DECL_VAR_OUT, + .num_outputs = 2, + .kind = NodeKind2::Declaration + }, + { + .type_id = NodeTypeID::Decl, + .name = "decl", + .desc = "Compile-time entry point", + .output_ports = P2_DECL_OUT, + .num_outputs = 1, + .kind = NodeKind2::Declaration + }, + { + .type_id = NodeTypeID::DeclEvent, + .name = "decl_event", + .desc = "Declare event", + .input_ports = P2_DECL_EVENT_IN, + .num_inputs = 3, + .output_ports = P2_NEXT, + .num_outputs = 1, + .kind = NodeKind2::Declaration + }, + { + .type_id = NodeTypeID::DeclImport, + .name = "decl_import", + .desc = "Import module", + .input_ports = P2_DECL_IMPORT_IN, + .num_inputs = 2, + .output_ports = P2_NEXT, + .num_outputs = 1, + .kind = NodeKind2::Declaration + }, + { + .type_id = NodeTypeID::Ffi, + .name = "ffi", + .desc = "Declare external function", + .input_ports = P2_FFI_IN, + .num_inputs = 3, + .output_ports = P2_NEXT, + .num_outputs = 1, + .kind = NodeKind2::Declaration + }, + { + .type_id = NodeTypeID::Call, + .name = "call", + .desc = "Call function", + .input_ports = P2_CALL_IN, + .num_inputs = 1, + .output_ports = P2_RESULT, + .num_outputs = 1, + .va_args = &P2_VA_ARG + }, + { + .type_id = NodeTypeID::CallBang, + .name = "call!", + .desc = "Call function (bang)", + .input_ports = P2_CALL_BANG_IN, + .num_inputs = 2, + .output_ports = P2_NEXT_RESULT, + .num_outputs = 2, + .kind = NodeKind2::Banged, + .va_args = &P2_VA_ARG + }, + { + .type_id = NodeTypeID::Erase, + .name = "erase", + .desc = "Erase from collection", + .input_ports = P2_ERASE_IN, + .num_inputs = 2, + .output_ports = P2_RESULT, + .num_outputs = 1 + }, + { + .type_id = NodeTypeID::OutputMixBang, + .name = "output_mix!", + .desc = "Mix into audio output", + .input_ports = P2_OUTPUT_MIX_IN, + .num_inputs = 2, + .kind = NodeKind2::Banged + }, + { + .type_id = NodeTypeID::Append, + .name = "append", + .desc = "Append to collection", + .input_ports = P2_APPEND_IN, + .num_inputs = 2, + .output_ports = P2_RESULT, + .num_outputs = 1 + }, + { + .type_id = NodeTypeID::AppendBang, + .name = "append!", + .desc = "Append to collection (bang)", + .input_ports = P2_APPEND_BANG_IN, + .num_inputs = 3, + .output_ports = P2_NEXT_RESULT, + .num_outputs = 2, + .kind = NodeKind2::Banged + }, + { + .type_id = NodeTypeID::Store, + .name = "store", + .desc = "Store value", + .input_ports = P2_STORE_IN, + .num_inputs = 2 + }, + { + .type_id = NodeTypeID::StoreBang, + .name = "store!", + .desc = "Store value (bang)", + .input_ports = P2_STORE_BANG_IN, + .num_inputs = 3, + .output_ports = P2_NEXT, + .num_outputs = 1, + .kind = NodeKind2::Banged + }, + { + .type_id = NodeTypeID::EventBang, + .name = "event!", + .desc = "Event source", + .output_ports = P2_EVENT_OUT, + .num_outputs = 1, + .kind = NodeKind2::Event + }, + { + .type_id = NodeTypeID::OnKeyDownBang, + .name = "on_key_down!", + .desc = "(removed)", + .kind = NodeKind2::Special + }, + { + .type_id = NodeTypeID::OnKeyUpBang, + .name = "on_key_up!", + .desc = "(removed)", + .kind = NodeKind2::Special + }, + { + .type_id = NodeTypeID::SelectBang, + .name = "select!", + .desc = "Branch on condition", + .input_ports = P2_SELECT_BANG_IN, + .num_inputs = 2, + .output_ports = P2_SELECT_BANG_OUT, + .num_outputs = 3, + .kind = NodeKind2::Banged + }, + { + .type_id = NodeTypeID::ExprBang, + .name = "expr!", + .desc = "Evaluate expression on bang", + .input_ports = P2_EXPR_BANG_IN, + .num_inputs = 1, + .output_ports = P2_NEXT, + .num_outputs = 1, + .kind = NodeKind2::Banged + }, + { + .type_id = NodeTypeID::EraseBang, + .name = "erase!", + .desc = "Erase from collection (bang)", + .input_ports = P2_ERASE_BANG_IN, + .num_inputs = 3, + .output_ports = P2_NEXT_RESULT, + .num_outputs = 2, + .kind = NodeKind2::Banged + }, + { + .type_id = NodeTypeID::Iterate, + .name = "iterate", + .desc = "Iterate collection", + .input_ports = P2_ITERATE_IN, + .num_inputs = 2 + }, + { + .type_id = NodeTypeID::IterateBang, + .name = "iterate!", + .desc = "Iterate collection (bang)", + .input_ports = P2_ITERATE_BANG_IN, + .num_inputs = 3, + .output_ports = P2_NEXT, + .num_outputs = 1, + .kind = NodeKind2::Banged + }, + { + .type_id = NodeTypeID::Next, + .name = "next", + .desc = "Advance iterator", + .input_ports = P2_VALUE, + .num_inputs = 1, + .output_ports = P2_RESULT, + .num_outputs = 1 + }, + { + .type_id = NodeTypeID::Lock, + .name = "lock", + .desc = "Execute under mutex lock", + .input_ports = P2_LOCK_IN, + .num_inputs = 2, + .va_args = &P2_VA_PARAM + }, + { + .type_id = NodeTypeID::LockBang, + .name = "lock!", + .desc = "Execute under mutex lock (bang)", + .input_ports = P2_LOCK_BANG_IN, + .num_inputs = 3, + .output_ports = P2_NEXT, + .num_outputs = 1, + .kind = NodeKind2::Banged, + .va_args = &P2_VA_PARAM + }, + { + .type_id = NodeTypeID::ResizeBang, + .name = "resize!", + .desc = "Resize vector", + .input_ports = P2_RESIZE_IN, + .num_inputs = 3, + .output_ports = P2_NEXT, + .num_outputs = 1, + .kind = NodeKind2::Banged + }, + { + .type_id = NodeTypeID::Cast, + .name = "cast", + .desc = "Cast value to type", + .input_ports = P2_VALUE, + .num_inputs = 1, + .output_ports = P2_RESULT, + .num_outputs = 1 + }, + { + .type_id = NodeTypeID::Label, + .name = "label", + .desc = "Text label", + .kind = NodeKind2::Special + }, + { + .type_id = NodeTypeID::Deref, + .name = "deref", + .desc = "Dereference iterator (internal)", + .input_ports = P2_VALUE, + .num_inputs = 1, + .output_ports = P2_RESULT, + .num_outputs = 1 + }, + { + .type_id = NodeTypeID::Error, + .name = "error", + .desc = "Error: invalid node", + .kind = NodeKind2::Special + }, }; static constexpr int NUM_NODE_TYPES2 = sizeof(NODE_TYPES2) / sizeof(NODE_TYPES2[0]); diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 3d653b3..5d12377 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -92,9 +92,9 @@ struct PinMapping { } // Absent trailing optional ports: show as pins beyond parsed_args if (nt) { - for (int i = parsed_size; i < nt->num_inputs; i++) { - if (nt->input_ports[i].optional) { - m.pin_to_port.push_back(-3000 - i); // sentinel for absent optional at port i + for (int i = parsed_size; i < nt->total_inputs(); i++) { + if (i >= nt->num_inputs) { // port is in the optional range + m.pin_to_port.push_back(-3000 - i); m.base_count++; } } @@ -527,14 +527,13 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil if (pm.is_absent_optional(i)) { int port = pm.absent_port_index(i); - if (nt->input_ports && port < nt->num_inputs) - kind = nt->input_ports[port].kind; + if (auto* pd = nt->input_port(port)) kind = pd->kind; is_optional = true; } else if (pm.is_base(i)) { int port = pm.port_index(i); - if (nt->input_ports && port < nt->num_inputs) { - kind = nt->input_ports[port].kind; - is_optional = nt->input_ports[port].optional; + if (auto* pd = nt->input_port(port)) { + kind = pd->kind; + is_optional = pd->optional; } } else if (is_va) { kind = nt->va_args ? nt->va_args->kind : PortKind2::Data; @@ -605,12 +604,10 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil auto get_input_pin_name = [&](int i) -> const char* { if (pm.is_absent_optional(i)) { int port = pm.absent_port_index(i); - if (nt->input_ports && port < nt->num_inputs) - return nt->input_ports[port].name; + if (auto* pd = nt->input_port(port)) return pd->name; } else if (pm.is_base(i)) { int port = pm.port_index(i); - if (nt->input_ports && port < nt->num_inputs) - return nt->input_ports[port].name; + if (auto* pd = nt->input_port(port)) return pd->name; } else if (pm.is_va(i)) { return nt->va_args ? nt->va_args->name : "va"; } else if (pm.is_remap(i)) { @@ -628,10 +625,10 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil if (pm.is_absent_optional(i)) return PinShape2::Diamond; if (pm.is_base(i)) { int port = pm.port_index(i); - if (nt->input_ports && port < nt->num_inputs) { - if (nt->input_ports[port].kind == PortKind2::BangTrigger) return PinShape2::Square; - if (nt->input_ports[port].kind == PortKind2::Lambda) return PinShape2::TriangleDown; - if (nt->input_ports[port].optional) return PinShape2::Diamond; + if (auto* pd = nt->input_port(port)) { + if (pd->kind == PortKind2::BangTrigger) return PinShape2::Square; + if (pd->kind == PortKind2::Lambda) return PinShape2::TriangleDown; + if (pd->optional) return PinShape2::Diamond; } } else if (pm.is_va(i)) { return PinShape2::Diamond; From 789589e78a0f03cb123ac5162e3a78f4913fee44 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 11:46:25 +0200 Subject: [PATCH 31/86] hover on + pin. --- src/attoflow/editor2.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 5d12377..e015f27 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -660,12 +660,16 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil bool pin_hovered = false; // Check input pins for (int i = 0; i < layout.num_in; i++) { - if (pm.is_add_diamond(i)) continue; ImVec2 pp = layout.input_pin_pos(i); if (dist2(mouse, pp) < hit_r2) { - draw_highlight(pp, get_input_pin_shape(i)); + draw_highlight(pp, pm.is_add_diamond(i) ? PinShape2::Diamond : get_input_pin_shape(i)); ImGui::BeginTooltip(); - ImGui::Text("%s", get_input_pin_name(i)); + if (pm.is_add_diamond(i)) { + const char* va_name = nt->va_args ? nt->va_args->name : "arg"; + ImGui::Text("add %s", va_name); + } else { + ImGui::Text("%s", get_input_pin_name(i)); + } ImGui::EndTooltip(); pin_hovered = true; break; From f4fb0250bc47426793b7c584612fadc7cba43a72 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 11:49:49 +0200 Subject: [PATCH 32/86] optionally scaled tooltips --- src/attoflow/editor2.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index e015f27..bd95a68 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -59,6 +59,9 @@ static struct { // Net label colors ImU32 col_label_bg = IM_COL32(30, 30, 40, 200); ImU32 col_label_text = IM_COL32(180, 220, 255, 255); + + // Tooltip + float tooltip_scale = 1.0f; } S; // ─── Helpers ─── @@ -664,6 +667,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil if (dist2(mouse, pp) < hit_r2) { draw_highlight(pp, pm.is_add_diamond(i) ? PinShape2::Diamond : get_input_pin_shape(i)); ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); if (pm.is_add_diamond(i)) { const char* va_name = nt->va_args ? nt->va_args->name : "arg"; ImGui::Text("add %s", va_name); @@ -684,6 +688,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil draw_highlight(pp, kind == PortKind2::BangNext ? PinShape2::Square : PinShape2::Circle); const char* name = (nt->output_ports && i < nt->num_outputs) ? nt->output_ports[i].name : "out"; ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); ImGui::Text("%s", name); ImGui::EndTooltip(); pin_hovered = true; @@ -697,6 +702,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil if (dist2(mouse, gp) < hit_r2) { draw_highlight(gp, PinShape2::TriangleLeft); ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); ImGui::Text("as_lambda"); ImGui::EndTooltip(); pin_hovered = true; @@ -708,6 +714,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil if (dist2(mouse, bp) < hit_r2) { draw_highlight(bp, PinShape2::Square); ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); ImGui::Text("post_bang"); ImGui::EndTooltip(); pin_hovered = true; @@ -719,6 +726,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height) { ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); ImGui::Text("id: %s", id.c_str()); if (node.parsed_args) { ImGui::Text("parsed_args (%d):", (int)node.parsed_args->size()); From 596bbcf1a2c99e957010727dcb17ef3cbe6dfd89 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 11:52:29 +0200 Subject: [PATCH 33/86] Increase default font size a bit --- src/attoflow/sdl_imgui_window.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/attoflow/sdl_imgui_window.h b/src/attoflow/sdl_imgui_window.h index a90dc25..78f28a6 100644 --- a/src/attoflow/sdl_imgui_window.h +++ b/src/attoflow/sdl_imgui_window.h @@ -43,7 +43,7 @@ struct SdlImGuiWindow { ImGuiIO& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; ImFontConfig font_cfg; - font_cfg.SizePixels = 17.0f * dpi_scale; + font_cfg.SizePixels = 22.0f * dpi_scale; io.Fonts->AddFontDefault(&font_cfg); io.FontGlobalScale = 1.0f / dpi_scale; ImGui::StyleColorsDark(); @@ -68,7 +68,7 @@ struct SdlImGuiWindow { ImGuiIO& io = ImGui::GetIO(); io.Fonts->Clear(); ImFontConfig font_cfg; - font_cfg.SizePixels = 17.0f * dpi_scale; + font_cfg.SizePixels = 22.0f * dpi_scale; io.Fonts->AddFontDefault(&font_cfg); io.FontGlobalScale = 1.0f / dpi_scale; ImGui_ImplSDLRenderer3_DestroyFontsTexture(); From 44d53aae5035654431109df9682b5cb87c6d94d6 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 12:05:01 +0200 Subject: [PATCH 34/86] Renumber auto strings --- src/atto/graphbuilder.cpp | 63 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index 0c878c3..c036664 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -867,6 +867,69 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { } } + // ─── Re-ID: $auto-xxx → $a-N (compact hex IDs) ─── + { + // Build rename map for $auto- entries + std::map rename; + int next_id = 0; + for (auto& [id, _] : gb->entries) { + if (id.compare(0, 6, "$auto-") == 0) { + char buf[32]; + snprintf(buf, sizeof(buf), "$a-%x", next_id++); + rename[id] = buf; + } + } + + // Also rename net names that start with $auto- but aren't in entries + // (they appear as ArgNet2 first-values referencing $auto- prefixed names) + + // Helper: rename an id if it has a mapping + auto remap_id = [&](const std::string& id) -> std::string { + // Check exact match + auto it = rename.find(id); + if (it != rename.end()) return it->second; + // Check if it starts with a known $auto- prefix (e.g. "$auto-xxx-out0") + // Find the longest matching prefix + for (auto& [old_prefix, new_prefix] : rename) { + if (id.size() > old_prefix.size() && id.compare(0, old_prefix.size(), old_prefix) == 0) { + // Check the char after the prefix is a separator + char sep = id[old_prefix.size()]; + if (sep == '-' || sep == '_') { + return new_prefix + id.substr(old_prefix.size()); + } + } + } + return id; + }; + + // Helper: rename ArgNet2 in-place + auto remap_arg = [&](FlowArg2& a) { + if (auto* an = std::get_if(&a)) + an->first = remap_id(an->first); + }; + auto remap_args = [&](ParsedArgs2* pa) { + if (!pa) return; + for (auto& a : *pa) remap_arg(a); + }; + + // Rename all references inside nodes + for (auto& [id, entry] : gb->entries) { + if (!std::holds_alternative(*entry)) continue; + auto& node = std::get(*entry); + remap_args(node.parsed_args.get()); + remap_args(node.parsed_va_args.get()); + for (auto& r : node.remaps) r.first = remap_id(r.first); + for (auto& o : node.outputs) o.first = remap_id(o.first); + } + + // Rebuild entries map with new keys + std::map new_entries; + for (auto& [id, entry] : gb->entries) { + new_entries[remap_id(id)] = std::move(entry); + } + gb->entries = std::move(new_entries); + } + gb->compact(); return gb; From f2b387e12ccaaae78917434ac1976f64ccd36eff Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 12:06:07 +0200 Subject: [PATCH 35/86] next_id --- src/atto/graphbuilder.cpp | 8 ++++++++ src/atto/graphbuilder.h | 3 +++ 2 files changed, 11 insertions(+) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index c036664..db4c729 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -278,6 +278,14 @@ void GraphBuilder::compact() { } } +NodeId GraphBuilder::next_id() { + for (int n = 0; ; n++) { + char buf[32]; + snprintf(buf, sizeof(buf), "$a-%x", n); + if (!entries.count(buf)) return buf; + } +} + // ─── Deserializer ─── BuilderResult Deserializer::parse_node( diff --git a/src/atto/graphbuilder.h b/src/atto/graphbuilder.h index c5b149b..9a752d6 100644 --- a/src/atto/graphbuilder.h +++ b/src/atto/graphbuilder.h @@ -101,6 +101,9 @@ struct GraphBuilder { std::pair find_entity(const NodeId& id); // returns node or net void compact(); + + // Returns the next unused $a-N id + NodeId next_id(); }; // Deserializer: parses raw strings into FlowNodeBuilder, with error fallback. From 56f211ba7271b05ff6354714f38329d4ab72e25a Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 12:36:25 +0200 Subject: [PATCH 36/86] Polymorphy for Nodes --- src/atto/graphbuilder.cpp | 193 ++++++++++++++++---------------------- src/atto/graphbuilder.h | 45 +++++++-- src/attoflow/editor2.cpp | 141 ++++++++++++++-------------- src/attoflow/editor2.h | 4 +- 4 files changed, 193 insertions(+), 190 deletions(-) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graphbuilder.cpp index db4c729..a4540ba 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graphbuilder.cpp @@ -57,19 +57,9 @@ static std::vector parse_toml_array(const std::string& val) { return result; } -// ─── Validation helper ─── - -static void validate_weak_is_node(const BuilderEntryWeak& w) { - auto p = w.lock(); - if (!p) return; // expired is ok - if (!std::holds_alternative(*p)) - throw std::logic_error("NetBuilder: weak ref points to a NetBuilder, not a FlowNodeBuilder"); -} - // ─── NetBuilder ─── void NetBuilder::compact() { - validate(); destinations.erase( std::remove_if(destinations.begin(), destinations.end(), [](auto& w) { return w.expired(); }), destinations.end()); @@ -81,9 +71,6 @@ bool NetBuilder::unused() { } void NetBuilder::validate() const { - validate_weak_is_node(source); - for (auto& d : destinations) - validate_weak_is_node(d); } // ─── v2 parse/reconstruct ─── @@ -202,40 +189,40 @@ std::string FlowNodeBuilder::args_str() const { // ─── GraphBuilder ─── -FlowNodeBuilder& GraphBuilder::add_node(NodeId id, NodeTypeID type, std::shared_ptr args) { - auto entry = std::make_shared(FlowNodeBuilder{}); - auto& nb = std::get(*entry); - nb.type_id = type; - nb.parsed_args = std::move(args); - entries[std::move(id)] = entry; +std::shared_ptr GraphBuilder::add_node(NodeId id, NodeTypeID type, std::shared_ptr args) { + auto nb = std::make_shared(); + nb->type_id = type; + nb->parsed_args = std::move(args); + nb->id = id; + entries[std::move(id)] = nb; return nb; } void GraphBuilder::ensure_unconnected() { if (entries.count("$unconnected")) return; - auto entry = std::make_shared(NetBuilder{}); - auto& net = std::get(*entry); - net.is_the_unconnected = true; - net.auto_wire = true; - entries["$unconnected"] = entry; + auto net = std::make_shared(); + net->is_the_unconnected = true; + net->auto_wire = true; + net->id = "$unconnected"; + entries["$unconnected"] = net; } std::pair GraphBuilder::find_or_create_net(const NodeId& name, bool for_source) { auto it = entries.find(name); if (it != entries.end()) { - if (std::holds_alternative(*it->second)) { - if (for_source && !std::get(*it->second).source.expired()) + if (auto net = it->second->as_Net()) { + if (for_source && !net->source.expired()) throw std::logic_error("find_or_create_net(\"" + name + "\"): net already has a source"); return {it->first, it->second}; } // Exists as a node — don't overwrite return {it->first, nullptr}; } - auto entry = std::make_shared(NetBuilder{}); - auto& net = std::get(*entry); - net.auto_wire = (name.size() >= 6 && name.substr(0, 6) == "$auto-"); - entries[name] = entry; - return {entries.find(name)->first, entry}; + auto net = std::make_shared(); + net->auto_wire = (name.size() >= 6 && name.substr(0, 6) == "$auto-"); + net->id = name; + entries[name] = net; + return {entries.find(name)->first, net}; } BuilderEntryPtr GraphBuilder::find(const NodeId& id) { @@ -243,33 +230,22 @@ BuilderEntryPtr GraphBuilder::find(const NodeId& id) { return (it != entries.end()) ? it->second : nullptr; } -std::pair GraphBuilder::find_node(const NodeId& id) { +FlowNodeBuilderPtr GraphBuilder::find_node(const NodeId& id) { auto it = entries.find(id); - if (it == entries.end()) return {id, nullptr}; - if (!std::holds_alternative(*it->second)) - throw std::logic_error("find_node(\"" + id + "\"): entry exists but is a NetBuilder, not a FlowNodeBuilder"); - return {it->first, it->second}; + if (it == entries.end()) return nullptr; + return it->second->as_Node(); } -std::pair GraphBuilder::find_net(const NodeId& name) { +NetBuilderPtr GraphBuilder::find_net(const NodeId& name) { auto it = entries.find(name); - if (it == entries.end()) return {name, nullptr}; - if (!std::holds_alternative(*it->second)) - return {name, nullptr}; // exists as node, not net - return {it->first, it->second}; -} - -std::pair GraphBuilder::find_entity(const NodeId& id) { - auto it = entries.find(id); - if (it == entries.end()) return {id, nullptr}; - return {it->first, it->second}; + if (it == entries.end()) return nullptr; + return it->second->as_Net(); } void GraphBuilder::compact() { for (auto it = entries.begin(); it != entries.end(); ) { - if (std::holds_alternative(*it->second)) { - auto& net = std::get(*it->second); - if (!net.is_the_unconnected && net.unused()) { + if (auto net = it->second->as_Net()) { + if (!net->is_the_unconnected && net->unused()) { it = entries.erase(it); continue; } @@ -327,9 +303,10 @@ FlowNodeBuilder& Deserializer::parse_or_error( auto result = parse_node(gb, id, type, args); if (auto* p = std::get_if>(&result)) { - auto entry = std::make_shared(std::move(p->second)); + auto entry = std::make_shared(std::move(p->second)); + entry->id = p->first; gb->entries[p->first] = entry; - return std::get(*entry); + return *entry; } auto& error_msg = std::get(result); @@ -338,14 +315,14 @@ FlowNodeBuilder& Deserializer::parse_or_error( if (!args_joined.empty()) args_joined += " "; args_joined += a; } - FlowNodeBuilder nb; - nb.type_id = NodeTypeID::Error; - nb.parsed_args = std::make_shared(); - nb.parsed_args->push_back(ArgString2{type + " " + args_joined}); - nb.error = error_msg; - auto entry = std::make_shared(std::move(nb)); + auto entry = std::make_shared(); + entry->type_id = NodeTypeID::Error; + entry->parsed_args = std::make_shared(); + entry->parsed_args->push_back(ArgString2{type + " " + args_joined}); + entry->error = error_msg; + entry->id = id; gb->entries[id] = entry; - return std::get(*entry); + return *entry; } // ─── parse_atto ─── @@ -406,8 +383,8 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // Helper: wire a net and return ArgNet2 auto wire_output = [&](const std::string& net_name) -> ArgNet2 { auto [resolved, net_ptr] = gb->find_or_create_net(net_name, true); - if (net_ptr && std::holds_alternative(*net_ptr)) - std::get(*net_ptr).source = node_entry; + if (auto net = net_ptr ? net_ptr->as_Net() : nullptr) + net->source = node_entry; return {resolved, net_ptr}; }; @@ -506,13 +483,13 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { auto ptr = gb->find(resolved_name); if (ptr) { // If it's a net, register as destination - if (std::holds_alternative(*ptr)) - std::get(*ptr).destinations.push_back(node_entry); + if (auto net = ptr->as_Net()) + net->destinations.push_back(node_entry); return {resolved_name, ptr}; } // Not found yet — create as net auto [id, net_ptr] = gb->find_or_create_net(resolved_name); - std::get(*net_ptr).destinations.push_back(node_entry); + net_ptr->as_Net()->destinations.push_back(node_entry); return {id, net_ptr}; }; @@ -671,8 +648,8 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { for (auto& net_name : cur_inputs) { if (net_name.empty()) continue; auto [_, net_ptr] = gb->find_or_create_net(net_name); - if (net_ptr && std::holds_alternative(*net_ptr)) - std::get(*net_ptr).destinations.push_back(node_entry); + if (auto net = net_ptr ? net_ptr->as_Net() : nullptr) + net->destinations.push_back(node_entry); } } @@ -744,18 +721,18 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if (!pa) return; for (auto& a : *pa) { if (auto* an = std::get_if(&a)) { - if (!an->second || !std::holds_alternative(*an->second)) continue; + if (!an->second || !an->second->as_Net()) continue; auto actual = gb->find(an->first); - if (actual && std::holds_alternative(*actual)) + if (actual && actual->as_Node()) an->second = actual; } } }; for (auto& [id, entry] : gb->entries) { - if (!std::holds_alternative(*entry)) continue; - auto& node = std::get(*entry); - fixup_args(node.parsed_args.get()); - fixup_args(node.parsed_va_args.get()); + auto node_p = entry->as_Node(); + if (!node_p) continue; + fixup_args(node_p->parsed_args.get()); + fixup_args(node_p->parsed_va_args.get()); } } @@ -765,8 +742,9 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // Collect shadow ids std::vector shadow_ids; for (auto& [id, entry] : gb->entries) { - if (!std::holds_alternative(*entry)) continue; - if (std::get(*entry).shadow) + auto node_p = entry->as_Node(); + if (!node_p) continue; + if (node_p->shadow) shadow_ids.push_back(id); } @@ -777,23 +755,22 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { std::string parent_id = shadow_id.substr(0, underscore_s); int arg_index = std::stoi(shadow_id.substr(underscore_s + 2)); - auto [_, parent_ptr] = gb->find_node(parent_id); + auto parent_ptr = gb->find_node(parent_id); if (!parent_ptr) continue; - auto& parent = std::get(*parent_ptr); - auto shadow_ptr = gb->find(shadow_id); + auto shadow_entry = gb->find(shadow_id); + auto shadow_ptr = shadow_entry ? shadow_entry->as_Node() : nullptr; if (!shadow_ptr) continue; - auto& shadow = std::get(*shadow_ptr); // Insert shadow expression into parent's parsed_args // Find the shadow's output net (e.g. "$auto-xxx_s0-out0") in parent's parsed_args and replace - if (parent.parsed_args && shadow.parsed_args && !shadow.parsed_args->empty()) { + if (parent_ptr->parsed_args && shadow_ptr->parsed_args && !shadow_ptr->parsed_args->empty()) { std::string shadow_out_prefix = shadow_id + "-out"; bool replaced = false; - for (auto& a : *parent.parsed_args) { + for (auto& a : *parent_ptr->parsed_args) { if (auto* an = std::get_if(&a)) { if (an->first.compare(0, shadow_out_prefix.size(), shadow_out_prefix) == 0) { - a = (*shadow.parsed_args)[0]; + a = (*shadow_ptr->parsed_args)[0]; replaced = true; break; } @@ -801,9 +778,9 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { } // Fallback: try positional insertion (for nodes without merged inputs) if (!replaced) { - while ((int)parent.parsed_args->size() <= arg_index) - parent.parsed_args->push_back(ArgString2{""}); - (*parent.parsed_args)[arg_index] = (*shadow.parsed_args)[0]; + while ((int)parent_ptr->parsed_args->size() <= arg_index) + parent_ptr->parsed_args->push_back(ArgString2{""}); + (*parent_ptr->parsed_args)[arg_index] = (*shadow_ptr->parsed_args)[0]; } } @@ -811,43 +788,39 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { auto sin_it = shadow_input_nets.find(shadow_id); if (sin_it != shadow_input_nets.end()) { auto& sin = sin_it->second; - // Shadow is an expr — no triggers, so inputs[i] maps directly to $i for (int i = 0; i < (int)sin.size(); i++) { - // Ensure parent remaps is large enough - while ((int)parent.remaps.size() <= i) - parent.remaps.push_back(ArgNet2{"$unconnected", unconnected_entry}); + while ((int)parent_ptr->remaps.size() <= i) + parent_ptr->remaps.push_back(ArgNet2{"$unconnected", unconnected_entry}); if (!sin[i].empty()) { auto net_ptr = gb->find(sin[i]); if (net_ptr) { - parent.remaps[i] = ArgNet2{sin[i], net_ptr}; + parent_ptr->remaps[i] = ArgNet2{sin[i], net_ptr}; - // Remove shadow from net's destinations, add parent instead - if (std::holds_alternative(*net_ptr)) { - auto& net = std::get(*net_ptr); - auto& dests = net.destinations; + if (auto net = net_ptr->as_Net()) { + auto& dests = net->destinations; dests.erase( std::remove_if(dests.begin(), dests.end(), - [&](auto& w) { return w.lock() == shadow_ptr; }), + [&](auto& w) { return w.lock() == shadow_entry; }), dests.end()); - net.destinations.push_back(parent_ptr); + net->destinations.push_back(parent_ptr); } } } } - // Update parent's rewrite_input_count to be the max across all shadows - if (parent.parsed_args) { - parent.parsed_args->rewrite_input_count = std::max( - parent.parsed_args->rewrite_input_count, (int)parent.remaps.size()); + if (parent_ptr->parsed_args) { + parent_ptr->parsed_args->rewrite_input_count = std::max( + parent_ptr->parsed_args->rewrite_input_count, (int)parent_ptr->remaps.size()); } } // Remove nets where shadow is source (internal shadow→parent plumbing) std::vector nets_to_remove; for (auto& [net_id, net_entry] : gb->entries) { - if (!std::holds_alternative(*net_entry)) continue; - auto src = std::get(*net_entry).source.lock(); - if (src == shadow_ptr) + auto net_as = net_entry->as_Net(); + if (!net_as) continue; + auto src = net_as->source.lock(); + if (src == shadow_entry) nets_to_remove.push_back(net_id); } for (auto& nid : nets_to_remove) @@ -859,8 +832,8 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // ─── Split parsed_args into base + va_args for nodes with va_args ─── for (auto& [id, entry] : gb->entries) { - if (!std::holds_alternative(*entry)) continue; - auto& node = std::get(*entry); + if (!entry->is(IdCategory::Node)) continue; + auto& node = *entry->as_Node(); auto* nt = find_node_type2(node.type_id); if (!nt || !nt->va_args || !node.parsed_args) continue; @@ -922,12 +895,12 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // Rename all references inside nodes for (auto& [id, entry] : gb->entries) { - if (!std::holds_alternative(*entry)) continue; - auto& node = std::get(*entry); - remap_args(node.parsed_args.get()); - remap_args(node.parsed_va_args.get()); - for (auto& r : node.remaps) r.first = remap_id(r.first); - for (auto& o : node.outputs) o.first = remap_id(o.first); + auto node_p = entry->as_Node(); + if (!node_p) continue; + remap_args(node_p->parsed_args.get()); + remap_args(node_p->parsed_va_args.get()); + for (auto& r : node_p->remaps) r.first = remap_id(r.first); + for (auto& o : node_p->outputs) o.first = remap_id(o.first); } // Rebuild entries map with new keys diff --git a/src/atto/graphbuilder.h b/src/atto/graphbuilder.h index 9a752d6..a2d9cfb 100644 --- a/src/atto/graphbuilder.h +++ b/src/atto/graphbuilder.h @@ -17,10 +17,34 @@ using BuilderError = std::string; // ─── Forward declarations & aliases ─── +enum class IdCategory { + Node, + Net +}; + struct FlowNodeBuilder; struct NetBuilder; -using BuilderEntry = std::variant; +struct BuilderEntry: std::enable_shared_from_this { + BuilderEntry(IdCategory category) : category(category) { } + virtual ~BuilderEntry() = default; + + NodeId id; + + bool is(IdCategory category) { return this->category == category; } + + std::shared_ptr as_Node() { + return std::dynamic_pointer_cast(shared_from_this()); + } + + std::shared_ptr as_Net() { + return std::dynamic_pointer_cast(shared_from_this()); + } + + private: + const IdCategory category; +}; + using BuilderEntryPtr = std::shared_ptr; using BuilderEntryWeak = std::weak_ptr; @@ -51,7 +75,9 @@ std::string reconstruct_args_str(const ParsedArgs2& args); // ─── Builder types ─── // Named wire — one source, many destinations (weak refs to BuilderEntry, must be FlowNodeBuilder). -struct NetBuilder { +struct NetBuilder: BuilderEntry { + NetBuilder(): BuilderEntry(IdCategory::Net) { } + bool auto_wire = false; bool is_the_unconnected = false; // true for the special $unconnected sentinel @@ -63,12 +89,16 @@ struct NetBuilder { void validate() const; }; +using NetBuilderPtr = std::shared_ptr; + // Remap: $N → net mapping (from folded shadow inputs) using Remaps = std::vector; using Outputs = std::vector; // A node under construction — holds structured parsed args instead of raw string. -struct FlowNodeBuilder { +struct FlowNodeBuilder: BuilderEntry { + FlowNodeBuilder(): BuilderEntry(IdCategory::Node) { } + NodeTypeID type_id = NodeTypeID::Unknown; std::shared_ptr parsed_args; // base pins (1:1 with descriptor) std::shared_ptr parsed_va_args; // va_args pins @@ -81,13 +111,15 @@ struct FlowNodeBuilder { std::string args_str() const; }; +using FlowNodeBuilderPtr = std::shared_ptr; + using BuilderResult = std::variant, BuilderError>; struct GraphBuilder { TypePool pool; std::map entries; - FlowNodeBuilder& add_node(NodeId id, NodeTypeID type, std::shared_ptr args); + std::shared_ptr add_node(NodeId id, NodeTypeID type, std::shared_ptr args); // Ensure the $unconnected sentinel net exists void ensure_unconnected(); @@ -96,9 +128,8 @@ struct GraphBuilder { BuilderEntryPtr find(const NodeId& id); - std::pair find_node(const NodeId& id); - std::pair find_net(const NodeId& name); - std::pair find_entity(const NodeId& id); // returns node or net + FlowNodeBuilderPtr find_node(const NodeId& id); + NetBuilderPtr find_net(const NodeId& name); void compact(); diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index bd95a68..ee12970 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -79,15 +79,15 @@ struct PinMapping { int add_pin_pos = -1; // position of +diamond (-1 if none) bool has_va = false; - static PinMapping build(const FlowNodeBuilder& node, const NodeType2* nt) { + static PinMapping build(const FlowNodeBuilderPtr& node, const NodeType2* nt) { PinMapping m; m.has_va = nt && nt->va_args != nullptr; - int parsed_size = node.parsed_args ? (int)node.parsed_args->size() : 0; + int parsed_size = node->parsed_args ? (int)node->parsed_args->size() : 0; // Base args: track which parsed_args indices are ArgNet2 - if (node.parsed_args) { + if (node->parsed_args) { for (int i = 0; i < parsed_size; i++) { - if (std::holds_alternative((*node.parsed_args)[i])) { + if (std::holds_alternative((*node->parsed_args)[i])) { m.pin_to_port.push_back(i); m.base_count++; } @@ -103,9 +103,9 @@ struct PinMapping { } } // Va_args - if (node.parsed_va_args) { - for (int i = 0; i < (int)node.parsed_va_args->size(); i++) { - if (std::holds_alternative((*node.parsed_va_args)[i])) { + if (node->parsed_va_args) { + for (int i = 0; i < (int)node->parsed_va_args->size(); i++) { + if (std::holds_alternative((*node->parsed_va_args)[i])) { m.pin_to_port.push_back(-(i + 1)); // negative = va_args index (1-based) m.va_count++; } @@ -117,7 +117,7 @@ struct PinMapping { m.pin_to_port.push_back(-1000); // sentinel for +diamond } // Remaps - for (int i = 0; i < (int)node.remaps.size(); i++) { + for (int i = 0; i < (int)node->remaps.size(); i++) { m.pin_to_port.push_back(-2000 - i); // sentinel for remap i } return m; @@ -154,10 +154,10 @@ struct NodeLayout { } }; -static NodeLayout compute_node_layout(const FlowNodeBuilder& node, ImVec2 canvas_origin, float zoom) { - auto* nt = find_node_type2(node.type_id); +static NodeLayout compute_node_layout(const FlowNodeBuilderPtr& node, ImVec2 canvas_origin, float zoom) { + auto* nt = find_node_type2(node->type_id); std::string display = nt ? nt->name : "?"; - std::string args = node.args_str(); + std::string args = node->args_str(); if (!args.empty()) display += " " + args; float font_size = ImGui::GetFontSize() * zoom; @@ -167,9 +167,9 @@ static NodeLayout compute_node_layout(const FlowNodeBuilder& node, ImVec2 canvas auto pm = PinMapping::build(node, nt); int num_in = pm.total(); int num_out = nt ? nt->num_outputs : 1; - if (node.type_id == NodeTypeID::Expr || node.type_id == NodeTypeID::ExprBang) { - int args_count = node.parsed_args ? (int)node.parsed_args->size() : 0; - if (node.type_id == NodeTypeID::ExprBang) num_out = 1 + std::max(1, args_count); + if (node->type_id == NodeTypeID::Expr || node->type_id == NodeTypeID::ExprBang) { + int args_count = node->parsed_args ? (int)node->parsed_args->size() : 0; + if (node->type_id == NodeTypeID::ExprBang) num_out = 1 + std::max(1, args_count); else num_out = std::max(1, args_count); } @@ -178,8 +178,8 @@ static NodeLayout compute_node_layout(const FlowNodeBuilder& node, ImVec2 canvas float node_w = std::max({S.node_min_width * zoom, text_w, pin_w_top, pin_w_bot}); float node_h = S.node_height * zoom; - ImVec2 pos = {canvas_origin.x + node.position.x * zoom, - canvas_origin.y + node.position.y * zoom}; + ImVec2 pos = {canvas_origin.x + node->position.x * zoom, + canvas_origin.y + node->position.y * zoom}; return {pos, node_w, node_h, num_in, num_out, zoom}; } @@ -257,18 +257,18 @@ void Editor2Pane::draw() { // Draw nodes (skip shadows) for (auto& [id, entry] : gb_->entries) { - if (std::holds_alternative(*entry)) { - auto& node = std::get(*entry); - if (node.shadow) throw std::logic_error("Editor2Pane: shadow nodes must be folded before rendering (id: " + id + ")"); - draw_node(dl, id, node, canvas_origin); + if (auto node = entry->as_Node()) { + if (node->shadow) throw std::logic_error("Editor2Pane: shadow nodes must be folded before rendering (id: " + id + ")"); + draw_node(dl, node, canvas_origin); } } // Draw wires by iterating each node's inputs/outputs/remaps for (auto& [dst_id, dst_entry] : gb_->entries) { - if (!std::holds_alternative(*dst_entry)) continue; - auto& dst_node = std::get(*dst_entry); - auto* dst_nt = find_node_type2(dst_node.type_id); + auto dst_node = dst_entry->as_Node(); + if (!dst_node) continue; + + auto* dst_nt = find_node_type2(dst_node->type_id); if (!dst_nt) continue; auto dst_layout = compute_node_layout(dst_node, canvas_origin, canvas_zoom_); @@ -277,34 +277,33 @@ void Editor2Pane::draw() { auto draw_wire_to_pin = [&](int dst_pin, const BuilderEntryPtr& entry, const NodeId& net_id) { if (!entry) return; - FlowNodeBuilder* src_node_ptr = nullptr; + FlowNodeBuilderPtr src_node_ptr = nullptr; bool named = false; bool is_lambda = false; int source_pin = 0; - if (std::holds_alternative(*entry)) { - auto& net = std::get(*entry); - if (net.is_the_unconnected) return; - auto src_ptr = net.source.lock(); - if (!src_ptr || !std::holds_alternative(*src_ptr)) return; - src_node_ptr = &std::get(*src_ptr); - named = !net.auto_wire; - // Find which output pin this net is on + if (auto net = entry->as_Net()) { + if (net->is_the_unconnected) return; + auto src_ptr = net->source.lock(); + auto src_node = src_ptr ? src_ptr->as_Node() : nullptr; + if (!src_node) return; + src_node_ptr = src_node; + named = !net->auto_wire; for (int k = 0; k < (int)src_node_ptr->outputs.size(); k++) { if (src_node_ptr->outputs[k].second == entry) { source_pin = k; break; } } - } else if (std::holds_alternative(*entry)) { - src_node_ptr = &std::get(*entry); + } else if (auto node = entry->as_Node()) { + src_node_ptr = node; is_lambda = true; } else { return; } auto* src_nt = find_node_type2(src_node_ptr->type_id); - auto src_layout = compute_node_layout(*src_node_ptr, canvas_origin, canvas_zoom_); + auto src_layout = compute_node_layout(src_node_ptr, canvas_origin, canvas_zoom_); ImVec2 to = dst_layout.input_pin_pos(dst_pin); float th = S.wire_thickness * canvas_zoom_; @@ -358,20 +357,20 @@ void Editor2Pane::draw() { if (dst_pm.is_absent_optional(i)) continue; if (dst_pm.is_base(i)) { int port = dst_pm.port_index(i); - if (dst_node.parsed_args && port < (int)dst_node.parsed_args->size()) { - if (auto* an = std::get_if(&(*dst_node.parsed_args)[port])) + if (dst_node->parsed_args && port < (int)dst_node->parsed_args->size()) { + if (auto* an = std::get_if(&(*dst_node->parsed_args)[port])) draw_wire_to_pin(i, an->second, an->first); } } else if (dst_pm.is_va(i)) { int va_idx = -(dst_pm.port_index(i) + 1); - if (dst_node.parsed_va_args && va_idx < (int)dst_node.parsed_va_args->size()) { - if (auto* an = std::get_if(&(*dst_node.parsed_va_args)[va_idx])) + if (dst_node->parsed_va_args && va_idx < (int)dst_node->parsed_va_args->size()) { + if (auto* an = std::get_if(&(*dst_node->parsed_va_args)[va_idx])) draw_wire_to_pin(i, an->second, an->first); } } else if (dst_pm.is_remap(i)) { int ri = dst_pm.remap_index(i); - if (ri < (int)dst_node.remaps.size()) - draw_wire_to_pin(i, dst_node.remaps[ri].second, dst_node.remaps[ri].first); + if (ri < (int)dst_node->remaps.size()) + draw_wire_to_pin(i, dst_node->remaps[ri].second, dst_node->remaps[ri].first); } } } @@ -385,9 +384,9 @@ void Editor2Pane::draw() { dragging_node_.clear(); // Iterate in reverse so topmost (last drawn) is hit first for (auto it = gb_->entries.rbegin(); it != gb_->entries.rend(); ++it) { - if (!std::holds_alternative(*it->second)) continue; - auto& node = std::get(*it->second); - auto layout = compute_node_layout(node, canvas_origin, canvas_zoom_); + auto node_p = it->second->as_Node(); + if (!node_p) continue; + auto layout = compute_node_layout(node_p, canvas_origin, canvas_zoom_); if (mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height) { dragging_node_ = it->first; @@ -403,11 +402,11 @@ void Editor2Pane::draw() { } if (dragging_started_ && ImGui::IsMouseDragging(ImGuiMouseButton_Left) && !dragging_node_.empty()) { auto it = gb_->entries.find(dragging_node_); - if (it != gb_->entries.end() && std::holds_alternative(*it->second)) { - auto& node = std::get(*it->second); + auto drag_node = (it != gb_->entries.end()) ? it->second->as_Node() : nullptr; + if (drag_node) { ImVec2 delta = ImGui::GetIO().MouseDelta; - node.position.x += delta.x / canvas_zoom_; - node.position.y += delta.y / canvas_zoom_; + drag_node->position.x += delta.x / canvas_zoom_; + drag_node->position.y += delta.y / canvas_zoom_; } } if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { @@ -443,9 +442,9 @@ void Editor2Pane::draw() { // ─── Draw a node ─── -void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuilder& node, +void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, ImVec2 canvas_origin) { - auto* nt = find_node_type2(node.type_id); + auto* nt = find_node_type2(node->type_id); if (!nt) return; auto layout = compute_node_layout(node, canvas_origin, canvas_zoom_); @@ -454,15 +453,15 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil if (nt->is_special()) { // Display first arg without quotes std::string display; - if (node.parsed_args && !node.parsed_args->empty()) { - auto& a = (*node.parsed_args)[0]; + if (node->parsed_args && !node->parsed_args->empty()) { + auto& a = (*node->parsed_args)[0]; if (auto* s = std::get_if(&a)) display = s->value; else if (auto* e = std::get_if(&a)) display = e->expr; - else display = node.args_str(); + else display = node->args_str(); } float font_size = ImGui::GetFontSize() * canvas_zoom_; - bool is_error = (node.type_id == NodeTypeID::Error); + bool is_error = (node->type_id == NodeTypeID::Error); if (is_error) { // Error: red box @@ -485,11 +484,11 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil // Display text std::string display = nt->name; - std::string args = node.args_str(); + std::string args = node->args_str(); if (!args.empty()) display += " " + args; - bool selected = selected_nodes_.count(id); - bool has_error = !node.error.empty(); + bool selected = selected_nodes_.count(node->id); + bool has_error = !node->error.empty(); ImU32 col = has_error ? S.col_node_err : (selected ? S.col_node_sel : S.col_node); dl->AddRectFilled(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, @@ -727,11 +726,11 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height) { ImGui::BeginTooltip(); ImGui::SetWindowFontScale(S.tooltip_scale); - ImGui::Text("id: %s", id.c_str()); - if (node.parsed_args) { - ImGui::Text("parsed_args (%d):", (int)node.parsed_args->size()); - for (int i = 0; i < (int)node.parsed_args->size(); i++) { - auto& a = (*node.parsed_args)[i]; + ImGui::Text("id: %s", node->id.c_str()); + if (node->parsed_args) { + ImGui::Text("parsed_args (%d):", (int)node->parsed_args->size()); + for (int i = 0; i < (int)node->parsed_args->size(); i++) { + auto& a = (*node->parsed_args)[i]; if (auto* n = std::get_if(&a)) ImGui::Text(" [%d] net: %s", i, n->first.c_str()); else if (auto* e = std::get_if(&a)) @@ -742,10 +741,10 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil ImGui::Text(" [%d] num: %g", i, v->value); } } - if (node.parsed_va_args && !node.parsed_va_args->empty()) { - ImGui::Text("parsed_va_args (%d):", (int)node.parsed_va_args->size()); - for (int i = 0; i < (int)node.parsed_va_args->size(); i++) { - auto& a = (*node.parsed_va_args)[i]; + if (node->parsed_va_args && !node->parsed_va_args->empty()) { + ImGui::Text("parsed_va_args (%d):", (int)node->parsed_va_args->size()); + for (int i = 0; i < (int)node->parsed_va_args->size(); i++) { + auto& a = (*node->parsed_va_args)[i]; if (auto* n = std::get_if(&a)) ImGui::Text(" [%d] net: %s", i, n->first.c_str()); else if (auto* e = std::get_if(&a)) @@ -756,15 +755,15 @@ void Editor2Pane::draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuil ImGui::Text(" [%d] num: %g", i, v->value); } } - if (!node.remaps.empty()) { - ImGui::Text("remaps (%d):", (int)node.remaps.size()); - for (int i = 0; i < (int)node.remaps.size(); i++) - ImGui::Text(" $%d -> %s", i, node.remaps[i].first.c_str()); + if (!node->remaps.empty()) { + ImGui::Text("remaps (%d):", (int)node->remaps.size()); + for (int i = 0; i < (int)node->remaps.size(); i++) + ImGui::Text(" $%d -> %s", i, node->remaps[i].first.c_str()); } ImGui::EndTooltip(); } } -void Editor2Pane::draw_net(ImDrawList*, const NodeId&, const NetBuilder&, ImVec2) { +void Editor2Pane::draw_net(ImDrawList*, const NetBuilderPtr&, ImVec2) { // Unused — wires drawn per-node in draw() } diff --git a/src/attoflow/editor2.h b/src/attoflow/editor2.h index f66a95e..3186ba3 100644 --- a/src/attoflow/editor2.h +++ b/src/attoflow/editor2.h @@ -38,8 +38,8 @@ class Editor2Pane { int editing_link_id_ = -1; // not used yet, placeholder // Drawing helpers - void draw_node(ImDrawList* dl, const NodeId& id, const FlowNodeBuilder& node, + void draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, ImVec2 canvas_origin); - void draw_net(ImDrawList* dl, const NodeId& id, const NetBuilder& net, + void draw_net(ImDrawList* dl, const NetBuilderPtr& net, ImVec2 canvas_origin); }; From 9a7b2bde0d09218c0a22b730603713abc1fa58b5 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 12:38:28 +0200 Subject: [PATCH 37/86] more editor2 cleanups --- src/attoflow/editor2.cpp | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index ee12970..979cac1 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -277,7 +277,7 @@ void Editor2Pane::draw() { auto draw_wire_to_pin = [&](int dst_pin, const BuilderEntryPtr& entry, const NodeId& net_id) { if (!entry) return; - FlowNodeBuilderPtr src_node_ptr = nullptr; + FlowNodeBuilderPtr src_node = nullptr; bool named = false; bool is_lambda = false; int source_pin = 0; @@ -285,25 +285,24 @@ void Editor2Pane::draw() { if (auto net = entry->as_Net()) { if (net->is_the_unconnected) return; auto src_ptr = net->source.lock(); - auto src_node = src_ptr ? src_ptr->as_Node() : nullptr; + src_node = src_ptr ? src_ptr->as_Node() : nullptr; if (!src_node) return; - src_node_ptr = src_node; named = !net->auto_wire; - for (int k = 0; k < (int)src_node_ptr->outputs.size(); k++) { - if (src_node_ptr->outputs[k].second == entry) { + for (int k = 0; k < (int)src_node->outputs.size(); k++) { + if (src_node->outputs[k].second == entry) { source_pin = k; break; } } } else if (auto node = entry->as_Node()) { - src_node_ptr = node; + src_node = node; is_lambda = true; } else { return; } - auto* src_nt = find_node_type2(src_node_ptr->type_id); - auto src_layout = compute_node_layout(src_node_ptr, canvas_origin, canvas_zoom_); + auto* src_nt = find_node_type2(src_node->type_id); + auto src_layout = compute_node_layout(src_node, canvas_origin, canvas_zoom_); ImVec2 to = dst_layout.input_pin_pos(dst_pin); float th = S.wire_thickness * canvas_zoom_; @@ -384,9 +383,9 @@ void Editor2Pane::draw() { dragging_node_.clear(); // Iterate in reverse so topmost (last drawn) is hit first for (auto it = gb_->entries.rbegin(); it != gb_->entries.rend(); ++it) { - auto node_p = it->second->as_Node(); - if (!node_p) continue; - auto layout = compute_node_layout(node_p, canvas_origin, canvas_zoom_); + auto node = it->second->as_Node(); + if (!node) continue; + auto layout = compute_node_layout(node, canvas_origin, canvas_zoom_); if (mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height) { dragging_node_ = it->first; From 85bc2fc6d192a46376dc2b09304fc840c43aa602 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 14:39:36 +0200 Subject: [PATCH 38/86] editor2: non overlap --- src/attoflow/editor2.cpp | 52 ++++++++++++++++++++++++++++++++++++++-- src/attoflow/editor2.h | 1 + 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 979cac1..4a7dc73 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -392,6 +392,25 @@ void Editor2Pane::draw() { selected_nodes_.clear(); selected_nodes_.insert(dragging_node_); dragging_started_ = true; + // Check if already overlapping at drag start + drag_was_overlapping_ = false; + { + auto dl2 = compute_node_layout(node, {0,0}, 1.0f); + float pad = S.node_height * 0.5f; + for (auto& [oid, oe] : gb_->entries) { + if (oid == dragging_node_) continue; + auto on = oe->as_Node(); + if (!on) continue; + auto ol = compute_node_layout(on, {0,0}, 1.0f); + if (node->position.x < on->position.x - pad + ol.width + pad * 2 && + node->position.x + dl2.width > on->position.x - pad && + node->position.y < on->position.y - pad + ol.height + pad * 2 && + node->position.y + dl2.height > on->position.y - pad) { + drag_was_overlapping_ = true; + break; + } + } + } break; } } @@ -404,8 +423,37 @@ void Editor2Pane::draw() { auto drag_node = (it != gb_->entries.end()) ? it->second->as_Node() : nullptr; if (drag_node) { ImVec2 delta = ImGui::GetIO().MouseDelta; - drag_node->position.x += delta.x / canvas_zoom_; - drag_node->position.y += delta.y / canvas_zoom_; + float new_x = drag_node->position.x + delta.x / canvas_zoom_; + float new_y = drag_node->position.y + delta.y / canvas_zoom_; + + // Compute proposed layout + auto proposed = compute_node_layout(drag_node, {0,0}, 1.0f); + float pw = proposed.width; + float ph = proposed.height; + + // Check overlap with all other nodes (0.5em padding) + // If the node was already overlapping at drag start, allow free movement + bool blocked = false; + if (!drag_was_overlapping_) { + float pad = S.node_height * 0.5f; + for (auto& [other_id, other_entry] : gb_->entries) { + if (other_id == dragging_node_) continue; + auto other_node = other_entry->as_Node(); + if (!other_node) continue; + auto other_layout = compute_node_layout(other_node, {0,0}, 1.0f); + float ox = other_node->position.x - pad, oy = other_node->position.y - pad; + float ow = other_layout.width + pad * 2, oh = other_layout.height + pad * 2; + if (new_x < ox + ow && new_x + pw > ox && + new_y < oy + oh && new_y + ph > oy) { + blocked = true; + break; + } + } + } + if (!blocked) { + drag_node->position.x = new_x; + drag_node->position.y = new_y; + } } } if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { diff --git a/src/attoflow/editor2.h b/src/attoflow/editor2.h index 3186ba3..2bafc25 100644 --- a/src/attoflow/editor2.h +++ b/src/attoflow/editor2.h @@ -35,6 +35,7 @@ class Editor2Pane { std::set selected_nodes_; NodeId dragging_node_; // node being dragged (empty = none) bool dragging_started_ = false; + bool drag_was_overlapping_ = false; // true if node was overlapping when drag began int editing_link_id_ = -1; // not used yet, placeholder // Drawing helpers From 733c6fad26ce11c1d7d9f6bf04adb06d5de6a6b4 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 14:48:34 +0200 Subject: [PATCH 39/86] Add Liberation Mono font support and license files --- CMakeLists.txt | 16 +++ src/attoflow/fonts/LICENSE-LiberationFonts | 102 ++++++++++++++++++ src/attoflow/fonts/LiberationMono-Bold.ttf | Bin 0 -> 307996 bytes src/attoflow/fonts/LiberationMono-Regular.ttf | Bin 0 -> 319508 bytes src/attoflow/sdl_imgui_window.h | 33 ++++-- 5 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 src/attoflow/fonts/LICENSE-LiberationFonts create mode 100644 src/attoflow/fonts/LiberationMono-Bold.ttf create mode 100644 src/attoflow/fonts/LiberationMono-Regular.ttf diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d76349..9bdb1dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,21 @@ if(ATTOLANG_BUILD_EDITOR) set(ATTO_NEEDS_IMGUI ON) include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/AttoDeps.cmake) + # Embed Liberation Mono font as C array + set(FONT_TTF "${CMAKE_CURRENT_SOURCE_DIR}/src/attoflow/fonts/LiberationMono-Regular.ttf") + set(FONT_HDR "${CMAKE_CURRENT_BINARY_DIR}/generated/LiberationMono_Regular.h") + file(READ "${FONT_TTF}" FONT_HEX HEX) + string(LENGTH "${FONT_HEX}" FONT_HEX_LEN) + math(EXPR FONT_SIZE "${FONT_HEX_LEN} / 2") + string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," FONT_BYTES "${FONT_HEX}") + file(WRITE "${FONT_HDR}" + "// Liberation Mono Regular - SIL Open Font License (auto-generated)\n" + "#pragma once\n" + "static const unsigned int LiberationMono_Regular_size = ${FONT_SIZE};\n" + "static const unsigned char LiberationMono_Regular_data[] = {\n" + "${FONT_BYTES}\n};\n" + ) + add_executable(attoflow src/attoflow/main.cpp src/attoflow/editor.cpp @@ -65,6 +80,7 @@ if(ATTOLANG_BUILD_EDITOR) target_include_directories(attoflow PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src/attoflow + ${CMAKE_CURRENT_BINARY_DIR}/generated ) if(WIN32) target_link_libraries(attoflow PRIVATE attolang SDL3::SDL3 imgui::imgui) diff --git a/src/attoflow/fonts/LICENSE-LiberationFonts b/src/attoflow/fonts/LICENSE-LiberationFonts new file mode 100644 index 0000000..aba73e8 --- /dev/null +++ b/src/attoflow/fonts/LICENSE-LiberationFonts @@ -0,0 +1,102 @@ +Digitized data copyright (c) 2010 Google Corporation + with Reserved Font Arimo, Tinos and Cousine. +Copyright (c) 2012 Red Hat, Inc. + with Reserved Font Name Liberation. + +This Font Software is licensed under the SIL Open Font License, +Version 1.1. + +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + +PREAMBLE The goals of the Open Font License (OFL) are to stimulate +worldwide development of collaborative font projects, to support the font +creation efforts of academic and linguistic communities, and to provide +a free and open framework in which fonts may be shared and improved in +partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. +The fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to +any document created using the fonts or their derivatives. + + + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. +This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components +as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting ? in part or in whole ? +any of the components of the Original Version, by changing formats or +by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer +or other person who contributed to the Font Software. + + +PERMISSION & CONDITIONS + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components,in + Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, + redistributed and/or sold with any software, provided that each copy + contains the above copyright notice and this license. These can be + included either as stand-alone text files, human-readable headers or + in the appropriate machine-readable metadata fields within text or + binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font + Name(s) unless explicit written permission is granted by the + corresponding Copyright Holder. This restriction only applies to the + primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font + Software shall not be used to promote, endorse or advertise any + Modified Version, except to acknowledge the contribution(s) of the + Copyright Holder(s) and the Author(s) or with their explicit written + permission. + +5) The Font Software, modified or unmodified, in part or in whole, must + be distributed entirely under this license, and must not be distributed + under any other license. The requirement for fonts to remain under + this license does not apply to any document created using the Font + Software. + + + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + + + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. + diff --git a/src/attoflow/fonts/LiberationMono-Bold.ttf b/src/attoflow/fonts/LiberationMono-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2e46737ac7ab3b2d31671d5a705bc3b1b47b38eb GIT binary patch literal 307996 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�x2=lwhh$ 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�~f)6vdvIuU6H;9r}yk(wD_5j|f7ngD*S&QD#J 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ⅆQ{WSD+fKJDJxW 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 literal 0 HcmV?d00001 diff --git a/src/attoflow/fonts/LiberationMono-Regular.ttf b/src/attoflow/fonts/LiberationMono-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e774859cba4b973ea0742bab4bc18df5ef5442a7 GIT binary patch literal 319508 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

(JHv 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

    S<%!&nsWJ?1J;NZW=bEN8j+Y;(k@pGlmSX%)MaY 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�?s)YUWU z>KS$QjJkS8T|J|&o>5oNsHgXAD^o%-sMjbt)j-F9R�qk)Xg*M<{5SKjJkP7-8{2z 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 z

    ilCiL zqf%_ea$#F$(H(C}Fy$1>7x@gNYkPpbVNzDt8^sg>z4+SJ)f;eD|!wLNyT zk#1Zo(YXK2Gy8z|;&RXIoz6(y<3HgU&iTnOyklF&%yMolU=#X9_+OH{^R4Y{FqyL%xyfr1?!IQ!J^}5xy3_Blg@Cg49MSpuba0DM^T>ZVGRqO zvRCr+|HCCO`45+z*Zeix&u`0rYmLi)z|?2W^Z(W4e|wF~ z&wq`s_s$yUx4+>v&u{Cq=J{=V*F3-N&o$5g#6Q_ydw*^D8)IY@7~6(%bjw#ixSlQX zNfXXjNVsY()0Z0p>E2o)Uo9k!?v8FyV^dv-qgqR z5~bUef)so0SJch4nt9B%wpPWI<;t;njx6-XGKv2Tx3DwWeS8=?5AHo(tHb5%dH##% zAXa#LkTbpI-zR)YiaW}1GyA6I(q-W{a0o_H$dOy(pzzK;+T))NE|ssKM}_%IL^dzK8~Et zUykD{9M|Ev3CFEC?!vJM$AdT?!|^nZTpM~F$6_2Gf|GBX^Wime*I(nDYhI?wi8pPi z1KzFmxzuZ~cmB(N$3_QlSjkgQ`(x8uyUS4ng32{Ur`@3|G@H# z;g##N#wl1`+c$gl71IC7GRteg^74V!eQu$D*8F$;7T22mC(2)4ng2trUi073e=!!yjvRsD=B%U~wkbt7KYRUZ z^1NzFxVPZ0zMK`yT#elP3psa3;G>-*69y&q7cZ9omUiIWigOGo+x%5R7ktvWySFxv zK`TVvaa4dc+`rs+k=vT@BFCHWBKI=iMNT*0MRuC+B9FyNdi=w8Lii94dgQ+w%dba! zRxWAUv^gEl(CMu$Go6j|t(%;Y`94t2Xyg~>3S0l&Ri+FNEB^;T`yK9Vj$AeQihul7 z(cdxuE&#vp{Oh3H&)@9hT5%Hn)8Xgu=S9Ep+oTzv6!6}Z*o&Wp%c;SRy~EhDkQemi`3ILSRVFDLB6omyf--F=~+zECGZ zxsL4xFZYFB=?lHu7kbTv@Vjy-f4e)t8G^4Q zg!f9=3iqAh?fkrv{Ei9bc?LP_hO@9L=zVW7W=nG^i~DlD+ZVzYOT6;^zR;4s&>~cSc~Yq6&9uyWn}q`~ #include #include +#include "LiberationMono_Regular.h" // Wraps an SDL3 window + renderer + ImGui context. // Each instance is an independent ImGui context, enabling multi-window apps. @@ -42,9 +43,15 @@ struct SdlImGuiWindow { ImGui::SetCurrentContext(imgui_ctx); ImGuiIO& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; - ImFontConfig font_cfg; - font_cfg.SizePixels = 22.0f * dpi_scale; - io.Fonts->AddFontDefault(&font_cfg); + { + float font_size = 16.0f * dpi_scale; + ImFontConfig font_cfg; + font_cfg.FontDataOwnedByAtlas = false; + io.Fonts->AddFontFromMemoryTTF( + (void*)LiberationMono_Regular_data, + LiberationMono_Regular_size, + font_size, &font_cfg); + } io.FontGlobalScale = 1.0f / dpi_scale; ImGui::StyleColorsDark(); ImGui_ImplSDL3_InitForSDLRenderer(window, renderer); @@ -67,9 +74,23 @@ struct SdlImGuiWindow { ImGui::SetCurrentContext(imgui_ctx); ImGuiIO& io = ImGui::GetIO(); io.Fonts->Clear(); - ImFontConfig font_cfg; - font_cfg.SizePixels = 22.0f * dpi_scale; - io.Fonts->AddFontDefault(&font_cfg); + { + float font_size = 16.0f * dpi_scale; + ImFont* font = nullptr; + const char* font_paths[] = { + "fonts/LiberationMono-Regular.ttf", + "../src/attoflow/fonts/LiberationMono-Regular.ttf", + "src/attoflow/fonts/LiberationMono-Regular.ttf", + nullptr + }; + for (auto* p = font_paths; *p && !font; p++) + font = io.Fonts->AddFontFromFileTTF(*p, font_size); + if (!font) { + ImFontConfig font_cfg; + font_cfg.SizePixels = font_size; + io.Fonts->AddFontDefault(&font_cfg); + } + } io.FontGlobalScale = 1.0f / dpi_scale; ImGui_ImplSDLRenderer3_DestroyFontsTexture(); io.Fonts->Build(); From d480da908f016207b3058a39a10d907907923646 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 14:52:13 +0200 Subject: [PATCH 40/86] graphbuilder -> graph_builder --- CMakeLists.txt | 2 +- scenes/klavier/main.atto | 143 +----------------- src/atto/args.cpp | 2 +- .../{graphbuilder.cpp => graph_builder.cpp} | 2 +- src/atto/{graphbuilder.h => graph_builder.h} | 0 src/attoflow/editor2.cpp | 2 +- src/attoflow/editor2.h | 2 +- 7 files changed, 13 insertions(+), 140 deletions(-) rename src/atto/{graphbuilder.cpp => graph_builder.cpp} (99%) rename src/atto/{graphbuilder.h => graph_builder.h} (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9bdb1dd..626f89e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ add_library(attolang STATIC src/atto/graph_index.cpp src/atto/shadow.cpp src/atto/symbol_table.cpp - src/atto/graphbuilder.cpp + src/atto/graph_builder.cpp ) target_include_directories(attolang PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src diff --git a/scenes/klavier/main.atto b/scenes/klavier/main.atto index 69ee963..105b641 100644 --- a/scenes/klavier/main.atto +++ b/scenes/klavier/main.atto @@ -701,26 +701,10 @@ position = [551.91, 422.962] id = "$auto-e073eb5950485587_s1" type = "expr" shadow = true -args = ["(osc:&osc_def)"] +args = ["(osc:&osc_def)->osc_res"] outputs = ["$auto-e073eb5950485587_s1-out0"] position = [551.91, 362.962] -[[node]] -id = "$auto-e073eb5950485587_s2" -type = "expr" -shadow = true -args = ["->"] -outputs = ["$auto-e073eb5950485587_s2-out0"] -position = [551.91, 302.962] - -[[node]] -id = "$auto-e073eb5950485587_s3" -type = "expr" -shadow = true -args = ["osc_res"] -outputs = ["$auto-e073eb5950485587_s3-out0"] -position = [551.91, 242.962] - [[node]] id = "$auto-fe155835bba6cd45_s0" type = "expr" @@ -733,26 +717,10 @@ position = [554.325, 467.55] id = "$auto-fe155835bba6cd45_s1" type = "expr" shadow = true -args = ["(osc:&osc_def)"] +args = ["(osc:&osc_def)->void"] outputs = ["$auto-fe155835bba6cd45_s1-out0"] position = [554.325, 407.55] -[[node]] -id = "$auto-fe155835bba6cd45_s2" -type = "expr" -shadow = true -args = ["->"] -outputs = ["$auto-fe155835bba6cd45_s2-out0"] -position = [554.325, 347.55] - -[[node]] -id = "$auto-fe155835bba6cd45_s3" -type = "expr" -shadow = true -args = ["void"] -outputs = ["$auto-fe155835bba6cd45_s3-out0"] -position = [554.325, 287.55] - [[node]] id = "$auto-09f161f1210cec4f_s0" type = "expr" @@ -957,26 +925,10 @@ position = [1467.98, 389.477] id = "$auto-20115e980dcd5b53_s1" type = "expr" shadow = true -args = ["(args:vector envs:vector)"] +args = ["(args:vector envs:vector)->void"] outputs = ["$auto-20115e980dcd5b53_s1-out0"] position = [1467.98, 329.477] -[[node]] -id = "$auto-20115e980dcd5b53_s2" -type = "expr" -shadow = true -args = ["->"] -outputs = ["$auto-20115e980dcd5b53_s2-out0"] -position = [1467.98, 269.477] - -[[node]] -id = "$auto-20115e980dcd5b53_s3" -type = "expr" -shadow = true -args = ["void"] -outputs = ["$auto-20115e980dcd5b53_s3-out0"] -position = [1467.98, 209.477] - [[node]] id = "$auto-0e02c497002f40c2_s0" type = "expr" @@ -1013,26 +965,10 @@ position = [557.37, 851.95] id = "$auto-c5373cf3d77e7979_s1" type = "expr" shadow = true -args = ["(midi_key:u8 freq:f32)"] +args = ["(midi_key:u8 freq:f32)->void"] outputs = ["$auto-c5373cf3d77e7979_s1-out0"] position = [557.37, 791.95] -[[node]] -id = "$auto-c5373cf3d77e7979_s2" -type = "expr" -shadow = true -args = ["->"] -outputs = ["$auto-c5373cf3d77e7979_s2-out0"] -position = [557.37, 731.95] - -[[node]] -id = "$auto-c5373cf3d77e7979_s3" -type = "expr" -shadow = true -args = ["void"] -outputs = ["$auto-c5373cf3d77e7979_s3-out0"] -position = [557.37, 671.95] - [[node]] id = "$auto-48a2c13cec7e5013_s0" type = "expr" @@ -1045,26 +981,10 @@ position = [561, 914] id = "$auto-48a2c13cec7e5013_s1" type = "expr" shadow = true -args = ["(midi_key:u8)"] +args = ["(midi_key:u8)->void"] outputs = ["$auto-48a2c13cec7e5013_s1-out0"] position = [561, 854] -[[node]] -id = "$auto-48a2c13cec7e5013_s2" -type = "expr" -shadow = true -args = ["->"] -outputs = ["$auto-48a2c13cec7e5013_s2-out0"] -position = [561, 794] - -[[node]] -id = "$auto-48a2c13cec7e5013_s3" -type = "expr" -shadow = true -args = ["void"] -outputs = ["$auto-48a2c13cec7e5013_s3-out0"] -position = [561, 734] - [[node]] id = "$auto-9facb8e5368e52c0_s0" type = "expr" @@ -1085,26 +1005,10 @@ position = [563.229, 970.563] id = "$auto-c17ebd09a44700e1_s1" type = "expr" shadow = true -args = ["()"] +args = ["()->void"] outputs = ["$auto-c17ebd09a44700e1_s1-out0"] position = [563.229, 910.563] -[[node]] -id = "$auto-c17ebd09a44700e1_s2" -type = "expr" -shadow = true -args = ["->"] -outputs = ["$auto-c17ebd09a44700e1_s2-out0"] -position = [563.229, 850.563] - -[[node]] -id = "$auto-c17ebd09a44700e1_s3" -type = "expr" -shadow = true -args = ["void"] -outputs = ["$auto-c17ebd09a44700e1_s3-out0"] -position = [563.229, 790.563] - [[node]] id = "$auto-50417175624d2751_s0" type = "expr" @@ -1117,25 +1021,10 @@ position = [566.535, 1028.41] id = "$auto-50417175624d2751_s1" type = "expr" shadow = true -args = ["()"] +args = ["()->void"] outputs = ["$auto-50417175624d2751_s1-out0"] position = [566.535, 968.41] -[[node]] -id = "$auto-50417175624d2751_s2" -type = "expr" -shadow = true -args = ["->"] -outputs = ["$auto-50417175624d2751_s2-out0"] -position = [566.535, 908.41] - -[[node]] -id = "$auto-50417175624d2751_s3" -type = "expr" -shadow = true -args = ["void"] -outputs = ["$auto-50417175624d2751_s3-out0"] -position = [566.535, 848.41] [[node]] id = "$auto-decl_on_quit_s0" @@ -1149,26 +1038,10 @@ position = [566.535, 1080] id = "$auto-decl_on_quit_s1" type = "expr" shadow = true -args = ["()"] +args = ["()->void"] outputs = ["$auto-decl_on_quit_s1-out0"] position = [566.535, 1020] -[[node]] -id = "$auto-decl_on_quit_s2" -type = "expr" -shadow = true -args = ["->"] -outputs = ["$auto-decl_on_quit_s2-out0"] -position = [566.535, 960] - -[[node]] -id = "$auto-decl_on_quit_s3" -type = "expr" -shadow = true -args = ["void"] -outputs = ["$auto-decl_on_quit_s3-out0"] -position = [566.535, 900] - [[node]] id = "$auto-445319c565ebdaa8_s1" type = "expr" diff --git a/src/atto/args.cpp b/src/atto/args.cpp index 147cd21..0851994 100644 --- a/src/atto/args.cpp +++ b/src/atto/args.cpp @@ -339,4 +339,4 @@ SplitResult split_args(const std::string& args_str) { return result; } -// (v2 types and functions moved to graphbuilder.h/cpp) +// (v2 types and functions moved to graph_builder.h/cpp) diff --git a/src/atto/graphbuilder.cpp b/src/atto/graph_builder.cpp similarity index 99% rename from src/atto/graphbuilder.cpp rename to src/atto/graph_builder.cpp index a4540ba..324a209 100644 --- a/src/atto/graphbuilder.cpp +++ b/src/atto/graph_builder.cpp @@ -1,4 +1,4 @@ -#include "graphbuilder.h" +#include "graph_builder.h" #include "node_types2.h" #include "expr.h" #include diff --git a/src/atto/graphbuilder.h b/src/atto/graph_builder.h similarity index 100% rename from src/atto/graphbuilder.h rename to src/atto/graph_builder.h diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 4a7dc73..cc69f94 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -1,5 +1,5 @@ #include "editor2.h" -#include "atto/graphbuilder.h" +#include "atto/graph_builder.h" #include "atto/node_types2.h" #include "imgui.h" #include diff --git a/src/attoflow/editor2.h b/src/attoflow/editor2.h index 2bafc25..6591224 100644 --- a/src/attoflow/editor2.h +++ b/src/attoflow/editor2.h @@ -1,5 +1,5 @@ #pragma once -#include "atto/graphbuilder.h" +#include "atto/graph_builder.h" #include "atto/node_types2.h" #include "imgui.h" #include From 09d3d1f7a56f06c3077a09f73ca68ba722eb78a4 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 15:12:12 +0200 Subject: [PATCH 41/86] ditry tracking --- src/atto/graph_builder.cpp | 112 ++++++++++++-------- src/atto/graph_builder.h | 204 +++++++++++++++++++++++++++---------- src/attoflow/editor2.cpp | 40 ++++---- 3 files changed, 245 insertions(+), 111 deletions(-) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index 324a209..c577615 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -57,17 +57,49 @@ static std::vector parse_toml_array(const std::string& val) { return result; } +// ─── Dirty-tracked setters ─── + +static void maybe_dirty(GraphBuilder* gb) { if (gb) gb->mark_dirty(); } +static void maybe_dirty(const std::shared_ptr& gb) { if (gb) gb->mark_dirty(); } + +void ArgNet2::id(const NodeId& v, GraphBuilder* gb) { id_ = v; maybe_dirty(gb); } +void ArgNet2::entry(std::shared_ptr v, GraphBuilder* gb) { entry_ = std::move(v); maybe_dirty(gb); } +void ArgNumber2::value(double v, GraphBuilder* gb) { value_ = v; maybe_dirty(gb); } +void ArgNumber2::is_float(bool v, GraphBuilder* gb) { is_float_ = v; maybe_dirty(gb); } +void ArgString2::value(const std::string& v, GraphBuilder* gb) { value_ = v; maybe_dirty(gb); } +void ArgExpr2::expr(const std::string& v, GraphBuilder* gb) { expr_ = v; maybe_dirty(gb); } + +// ─── ParsedArgs2 ─── + +void ParsedArgs2::push_back(FlowArg2 arg) { items_.push_back(std::move(arg)); maybe_dirty(owner); } +void ParsedArgs2::pop_back() { items_.pop_back(); maybe_dirty(owner); } +void ParsedArgs2::resize(int n) { items_.resize(n); maybe_dirty(owner); } +void ParsedArgs2::insert(std::vector::iterator pos, FlowArg2 arg) { items_.insert(pos, std::move(arg)); maybe_dirty(owner); } +void ParsedArgs2::clear() { items_.clear(); maybe_dirty(owner); } + +// ─── BuilderEntry ─── + +void BuilderEntry::id(const NodeId& v) { id_ = v; mark_dirty(); } +void BuilderEntry::mark_dirty() { maybe_dirty(owner_); } + +std::shared_ptr BuilderEntry::as_Node() { + return std::dynamic_pointer_cast(shared_from_this()); +} +std::shared_ptr BuilderEntry::as_Net() { + return std::dynamic_pointer_cast(shared_from_this()); +} + // ─── NetBuilder ─── void NetBuilder::compact() { - destinations.erase( - std::remove_if(destinations.begin(), destinations.end(), [](auto& w) { return w.expired(); }), - destinations.end()); + destinations().erase( + std::remove_if(destinations().begin(), destinations().end(), [](auto& w) { return w.expired(); }), + destinations().end()); } bool NetBuilder::unused() { compact(); - return source.expired() && destinations.empty(); + return source().expired() && destinations().empty(); } void NetBuilder::validate() const { @@ -155,18 +187,18 @@ std::string reconstruct_args_str(const ParsedArgs2& args) { if (!result.empty()) result += " "; std::visit([&](auto& v) { using T = std::decay_t; - if constexpr (std::is_same_v) result += v.first; + if constexpr (std::is_same_v) result += v.first(); else if constexpr (std::is_same_v) { - if (v.is_float) { + if (v.is_float()) { char buf[64]; - snprintf(buf, sizeof(buf), "%g", v.value); + snprintf(buf, sizeof(buf), "%g", v.value()); result += buf; } else { - result += std::to_string((long long)v.value); + result += std::to_string((long long)v.value()); } } - else if constexpr (std::is_same_v) result += "\"" + v.value + "\""; - else if constexpr (std::is_same_v) result += v.expr; + else if constexpr (std::is_same_v) result += "\"" + v.value() + "\""; + else if constexpr (std::is_same_v) result += v.expr(); }, a); } return result; @@ -190,20 +222,20 @@ std::string FlowNodeBuilder::args_str() const { // ─── GraphBuilder ─── std::shared_ptr GraphBuilder::add_node(NodeId id, NodeTypeID type, std::shared_ptr args) { - auto nb = std::make_shared(); + auto nb = std::make_shared(shared_from_this()); nb->type_id = type; nb->parsed_args = std::move(args); - nb->id = id; + nb->id(id); entries[std::move(id)] = nb; return nb; } void GraphBuilder::ensure_unconnected() { if (entries.count("$unconnected")) return; - auto net = std::make_shared(); - net->is_the_unconnected = true; - net->auto_wire = true; - net->id = "$unconnected"; + auto net = std::make_shared(shared_from_this()); + net->is_the_unconnected(true); + net->auto_wire(true); + net->id("$unconnected"); entries["$unconnected"] = net; } @@ -211,16 +243,15 @@ std::pair GraphBuilder::find_or_create_net(const NodeId auto it = entries.find(name); if (it != entries.end()) { if (auto net = it->second->as_Net()) { - if (for_source && !net->source.expired()) + if (for_source && !net->source().expired()) throw std::logic_error("find_or_create_net(\"" + name + "\"): net already has a source"); return {it->first, it->second}; } - // Exists as a node — don't overwrite return {it->first, nullptr}; } - auto net = std::make_shared(); - net->auto_wire = (name.size() >= 6 && name.substr(0, 6) == "$auto-"); - net->id = name; + auto net = std::make_shared(shared_from_this()); + net->auto_wire(name.size() >= 6 && name.substr(0, 6) == "$auto-"); + net->id(name); entries[name] = net; return {entries.find(name)->first, net}; } @@ -245,7 +276,7 @@ NetBuilderPtr GraphBuilder::find_net(const NodeId& name) { void GraphBuilder::compact() { for (auto it = entries.begin(); it != entries.end(); ) { if (auto net = it->second->as_Net()) { - if (!net->is_the_unconnected && net->unused()) { + if (!net->is_the_unconnected() && net->unused()) { it = entries.erase(it); continue; } @@ -304,7 +335,8 @@ FlowNodeBuilder& Deserializer::parse_or_error( if (auto* p = std::get_if>(&result)) { auto entry = std::make_shared(std::move(p->second)); - entry->id = p->first; + entry->id(p->first); + entry->owner(gb); gb->entries[p->first] = entry; return *entry; } @@ -315,12 +347,12 @@ FlowNodeBuilder& Deserializer::parse_or_error( if (!args_joined.empty()) args_joined += " "; args_joined += a; } - auto entry = std::make_shared(); + auto entry = std::make_shared(gb); entry->type_id = NodeTypeID::Error; entry->parsed_args = std::make_shared(); entry->parsed_args->push_back(ArgString2{type + " " + args_joined}); entry->error = error_msg; - entry->id = id; + entry->id(id); gb->entries[id] = entry; return *entry; } @@ -384,7 +416,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { auto wire_output = [&](const std::string& net_name) -> ArgNet2 { auto [resolved, net_ptr] = gb->find_or_create_net(net_name, true); if (auto net = net_ptr ? net_ptr->as_Net() : nullptr) - net->source = node_entry; + net->source(node_entry); return {resolved, net_ptr}; }; @@ -484,12 +516,12 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if (ptr) { // If it's a net, register as destination if (auto net = ptr->as_Net()) - net->destinations.push_back(node_entry); + net->destinations().push_back(node_entry); return {resolved_name, ptr}; } // Not found yet — create as net auto [id, net_ptr] = gb->find_or_create_net(resolved_name); - net_ptr->as_Net()->destinations.push_back(node_entry); + net_ptr->as_Net()->destinations().push_back(node_entry); return {id, net_ptr}; }; @@ -649,7 +681,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if (net_name.empty()) continue; auto [_, net_ptr] = gb->find_or_create_net(net_name); if (auto net = net_ptr ? net_ptr->as_Net() : nullptr) - net->destinations.push_back(node_entry); + net->destinations().push_back(node_entry); } } @@ -667,7 +699,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if (trim_nt && nb.parsed_args) { while ((int)nb.parsed_args->size() > trim_nt->num_inputs) { auto* an = std::get_if(&nb.parsed_args->back()); - if (!an || an->first != "$unconnected") break; + if (!an || an->first() != "$unconnected") break; nb.parsed_args->pop_back(); } } @@ -721,10 +753,10 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if (!pa) return; for (auto& a : *pa) { if (auto* an = std::get_if(&a)) { - if (!an->second || !an->second->as_Net()) continue; - auto actual = gb->find(an->first); + if (!an->second() || !an->second()->as_Net()) continue; + auto actual = gb->find(an->first()); if (actual && actual->as_Node()) - an->second = actual; + an->entry(actual); } } }; @@ -769,7 +801,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { bool replaced = false; for (auto& a : *parent_ptr->parsed_args) { if (auto* an = std::get_if(&a)) { - if (an->first.compare(0, shadow_out_prefix.size(), shadow_out_prefix) == 0) { + if (an->first().compare(0, shadow_out_prefix.size(), shadow_out_prefix) == 0) { a = (*shadow_ptr->parsed_args)[0]; replaced = true; break; @@ -798,12 +830,12 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { parent_ptr->remaps[i] = ArgNet2{sin[i], net_ptr}; if (auto net = net_ptr->as_Net()) { - auto& dests = net->destinations; + auto& dests = net->destinations(); dests.erase( std::remove_if(dests.begin(), dests.end(), [&](auto& w) { return w.lock() == shadow_entry; }), dests.end()); - net->destinations.push_back(parent_ptr); + net->destinations().push_back(parent_ptr); } } } @@ -819,7 +851,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { for (auto& [net_id, net_entry] : gb->entries) { auto net_as = net_entry->as_Net(); if (!net_as) continue; - auto src = net_as->source.lock(); + auto src = net_as->source().lock(); if (src == shadow_entry) nets_to_remove.push_back(net_id); } @@ -886,7 +918,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // Helper: rename ArgNet2 in-place auto remap_arg = [&](FlowArg2& a) { if (auto* an = std::get_if(&a)) - an->first = remap_id(an->first); + an->id(remap_id(an->first())); }; auto remap_args = [&](ParsedArgs2* pa) { if (!pa) return; @@ -899,8 +931,8 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if (!node_p) continue; remap_args(node_p->parsed_args.get()); remap_args(node_p->parsed_va_args.get()); - for (auto& r : node_p->remaps) r.first = remap_id(r.first); - for (auto& o : node_p->outputs) o.first = remap_id(o.first); + for (auto& r : node_p->remaps) r.id(remap_id(r.first())); + for (auto& o : node_p->outputs) o.id(remap_id(o.first())); } // Rebuild entries map with new keys diff --git a/src/atto/graph_builder.h b/src/atto/graph_builder.h index a2d9cfb..3a19867 100644 --- a/src/atto/graph_builder.h +++ b/src/atto/graph_builder.h @@ -15,97 +15,183 @@ using NodeId = std::string; using BuilderError = std::string; -// ─── Forward declarations & aliases ─── +// ─── Forward declarations ─── enum class IdCategory { Node, Net }; +struct GraphBuilder; struct FlowNodeBuilder; struct NetBuilder; -struct BuilderEntry: std::enable_shared_from_this { - BuilderEntry(IdCategory category) : category(category) { } - virtual ~BuilderEntry() = default; +// ─── Arg types with dirty tracking ─── + +struct ArgNet2 { + ArgNet2() = default; + ArgNet2(NodeId id, std::shared_ptr entry) : id_(std::move(id)), entry_(std::move(entry)) {} - NodeId id; + const NodeId& id() const { return id_; } + void id(const NodeId& v, GraphBuilder* gb = nullptr); - bool is(IdCategory category) { return this->category == category; } - - std::shared_ptr as_Node() { - return std::dynamic_pointer_cast(shared_from_this()); - } + const std::shared_ptr& entry() const { return entry_; } + void entry(std::shared_ptr v, GraphBuilder* gb = nullptr); - std::shared_ptr as_Net() { - return std::dynamic_pointer_cast(shared_from_this()); - } + // Convenience: access like old pair + const NodeId& first() const { return id_; } + const std::shared_ptr& second() const { return entry_; } - private: - const IdCategory category; +private: + NodeId id_; + std::shared_ptr entry_; }; -using BuilderEntryPtr = std::shared_ptr; -using BuilderEntryWeak = std::weak_ptr; +struct ArgNumber2 { + ArgNumber2() = default; + ArgNumber2(double v, bool f) : value_(v), is_float_(f) {} + + double value() const { return value_; } + void value(double v, GraphBuilder* gb = nullptr); -// ─── v2 argument types ─── + bool is_float() const { return is_float_; } + void is_float(bool v, GraphBuilder* gb = nullptr); + +private: + double value_ = 0; + bool is_float_ = false; +}; + +struct ArgString2 { + ArgString2() = default; + ArgString2(std::string v) : value_(std::move(v)) {} + + const std::string& value() const { return value_; } + void value(const std::string& v, GraphBuilder* gb = nullptr); + +private: + std::string value_; +}; -using ArgNet2 = std::pair; // resolved net ref from find_or_create_net -struct ArgNumber2 { double value; bool is_float; }; // 42, 3.14 -struct ArgString2 { std::string value; }; // "hello\"world" -struct ArgExpr2 { std::string expr; }; // expression (contains $N, @N, operators, etc.) +struct ArgExpr2 { + ArgExpr2() = default; + ArgExpr2(std::string e) : expr_(std::move(e)) {} + + const std::string& expr() const { return expr_; } + void expr(const std::string& v, GraphBuilder* gb = nullptr); + +private: + std::string expr_; +}; using FlowArg2 = std::variant; -struct ParsedArgs2 : std::vector { - using vector::vector; - int rewrite_input_count = 0; // count of unique $N refs across all expressions (contiguous from $0) +// ─── ParsedArgs2: vector wrapper with dirty tracking ─── + +struct ParsedArgs2 { + int rewrite_input_count = 0; + + // Read access + bool empty() const { return items_.empty(); } + int size() const { return (int)items_.size(); } + FlowArg2& operator[](int i) { return items_[i]; } + const FlowArg2& operator[](int i) const { return items_[i]; } + auto begin() { return items_.begin(); } + auto end() { return items_.end(); } + auto begin() const { return items_.begin(); } + auto end() const { return items_.end(); } + FlowArg2& back() { return items_.back(); } + const FlowArg2& back() const { return items_.back(); } + + // Write access (marks dirty) + void push_back(FlowArg2 arg); + void pop_back(); + void resize(int n); + void insert(typename std::vector::iterator pos, FlowArg2 arg); + void clear(); + + // Owner + std::shared_ptr owner; + +private: + std::vector items_; }; -struct GraphBuilder; // forward for parse_args_v2 +// ─── BuilderEntry base ─── -// Parse pre-split expressions into ParsedArgs2. Resolves $name tokens via gb. -using ParseResult = std::variant, std::string>; -ParseResult parse_args_v2(const std::shared_ptr& gb, - const std::vector& exprs, bool is_expr = false); +struct BuilderEntry: std::enable_shared_from_this { + BuilderEntry(IdCategory category, const std::shared_ptr& owner = nullptr) + : category_(category), owner_(owner) { } + virtual ~BuilderEntry() = default; -// Reconstruct a space-separated args string from ParsedArgs2 -std::string reconstruct_args_str(const ParsedArgs2& args); + const NodeId& id() const { return id_; } + void id(const NodeId& v); + + bool is(IdCategory cat) const { return category_ == cat; } + + std::shared_ptr as_Node(); + std::shared_ptr as_Net(); -// ─── Builder types ─── + std::shared_ptr owner() const { return owner_; } + void owner(const std::shared_ptr& gb) { owner_ = gb; } + +protected: + void mark_dirty(); + +private: + const IdCategory category_; + NodeId id_; + std::shared_ptr owner_; +}; + +using BuilderEntryPtr = std::shared_ptr; +using BuilderEntryWeak = std::weak_ptr; + +// ─── NetBuilder ─── -// Named wire — one source, many destinations (weak refs to BuilderEntry, must be FlowNodeBuilder). struct NetBuilder: BuilderEntry { - NetBuilder(): BuilderEntry(IdCategory::Net) { } + NetBuilder(const std::shared_ptr& owner = nullptr): BuilderEntry(IdCategory::Net, owner) { } + + bool auto_wire() const { return auto_wire_; } + void auto_wire(bool v) { auto_wire_ = v; } + + bool is_the_unconnected() const { return is_the_unconnected_; } + void is_the_unconnected(bool v) { is_the_unconnected_ = v; } - bool auto_wire = false; - bool is_the_unconnected = false; // true for the special $unconnected sentinel + const BuilderEntryWeak& source() const { return source_; } + void source(BuilderEntryWeak v) { source_ = std::move(v); mark_dirty(); } - BuilderEntryWeak source; - std::vector destinations; + std::vector& destinations() { return destinations_; } + const std::vector& destinations() const { return destinations_; } void compact(); bool unused(); void validate() const; + +private: + bool auto_wire_ = false; + bool is_the_unconnected_ = false; + BuilderEntryWeak source_; + std::vector destinations_; }; using NetBuilderPtr = std::shared_ptr; -// Remap: $N → net mapping (from folded shadow inputs) +// ─── FlowNodeBuilder ─── + using Remaps = std::vector; using Outputs = std::vector; -// A node under construction — holds structured parsed args instead of raw string. struct FlowNodeBuilder: BuilderEntry { - FlowNodeBuilder(): BuilderEntry(IdCategory::Node) { } + FlowNodeBuilder(const std::shared_ptr& owner = nullptr): BuilderEntry(IdCategory::Node, owner) { } NodeTypeID type_id = NodeTypeID::Unknown; - std::shared_ptr parsed_args; // base pins (1:1 with descriptor) - std::shared_ptr parsed_va_args; // va_args pins - Remaps remaps; // $N → net mapping (remaps[0] = net for $0, etc.) - Outputs outputs; // output pin → net mapping (1:1 with descriptor output_ports) + std::shared_ptr parsed_args; + std::shared_ptr parsed_va_args; + Remaps remaps; + Outputs outputs; Vec2 position = {0, 0}; - bool shadow = false; // only used during migration, must be false after folding + bool shadow = false; std::string error; std::string args_str() const; @@ -115,13 +201,14 @@ using FlowNodeBuilderPtr = std::shared_ptr; using BuilderResult = std::variant, BuilderError>; -struct GraphBuilder { +// ─── GraphBuilder ─── + +struct GraphBuilder : std::enable_shared_from_this { TypePool pool; std::map entries; std::shared_ptr add_node(NodeId id, NodeTypeID type, std::shared_ptr args); - // Ensure the $unconnected sentinel net exists void ensure_unconnected(); std::pair find_or_create_net(const NodeId& name, bool for_source = false); @@ -133,11 +220,26 @@ struct GraphBuilder { void compact(); - // Returns the next unused $a-N id NodeId next_id(); + + // Dirty tracking + void mark_dirty() { dirty_ = true; } + bool was_dirty() { bool d = dirty_; dirty_ = false; return d; } + +private: + bool dirty_ = false; }; -// Deserializer: parses raw strings into FlowNodeBuilder, with error fallback. +// ─── Parse/reconstruct helpers ─── + +using ParseResult = std::variant, std::string>; +ParseResult parse_args_v2(const std::shared_ptr& gb, + const std::vector& exprs, bool is_expr = false); + +std::string reconstruct_args_str(const ParsedArgs2& args); + +// ─── Deserializer ─── + struct Deserializer { static BuilderResult parse_node( const std::shared_ptr& gb, diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index cc69f94..5f00d74 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -283,13 +283,13 @@ void Editor2Pane::draw() { int source_pin = 0; if (auto net = entry->as_Net()) { - if (net->is_the_unconnected) return; - auto src_ptr = net->source.lock(); + if (net->is_the_unconnected()) return; + auto src_ptr = net->source().lock(); src_node = src_ptr ? src_ptr->as_Node() : nullptr; if (!src_node) return; - named = !net->auto_wire; + named = !net->auto_wire(); for (int k = 0; k < (int)src_node->outputs.size(); k++) { - if (src_node->outputs[k].second == entry) { + if (src_node->outputs[k].second() == entry) { source_pin = k; break; } @@ -358,18 +358,18 @@ void Editor2Pane::draw() { int port = dst_pm.port_index(i); if (dst_node->parsed_args && port < (int)dst_node->parsed_args->size()) { if (auto* an = std::get_if(&(*dst_node->parsed_args)[port])) - draw_wire_to_pin(i, an->second, an->first); + draw_wire_to_pin(i, an->second(), an->first()); } } else if (dst_pm.is_va(i)) { int va_idx = -(dst_pm.port_index(i) + 1); if (dst_node->parsed_va_args && va_idx < (int)dst_node->parsed_va_args->size()) { if (auto* an = std::get_if(&(*dst_node->parsed_va_args)[va_idx])) - draw_wire_to_pin(i, an->second, an->first); + draw_wire_to_pin(i, an->second(), an->first()); } } else if (dst_pm.is_remap(i)) { int ri = dst_pm.remap_index(i); if (ri < (int)dst_node->remaps.size()) - draw_wire_to_pin(i, dst_node->remaps[ri].second, dst_node->remaps[ri].first); + draw_wire_to_pin(i, dst_node->remaps[ri].second(), dst_node->remaps[ri].first()); } } } @@ -502,8 +502,8 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, std::string display; if (node->parsed_args && !node->parsed_args->empty()) { auto& a = (*node->parsed_args)[0]; - if (auto* s = std::get_if(&a)) display = s->value; - else if (auto* e = std::get_if(&a)) display = e->expr; + if (auto* s = std::get_if(&a)) display = s->value(); + else if (auto* e = std::get_if(&a)) display = e->expr(); else display = node->args_str(); } @@ -534,7 +534,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, std::string args = node->args_str(); if (!args.empty()) display += " " + args; - bool selected = selected_nodes_.count(node->id); + bool selected = selected_nodes_.count(node->id()); bool has_error = !node->error.empty(); ImU32 col = has_error ? S.col_node_err : (selected ? S.col_node_sel : S.col_node); @@ -773,19 +773,19 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height) { ImGui::BeginTooltip(); ImGui::SetWindowFontScale(S.tooltip_scale); - ImGui::Text("id: %s", node->id.c_str()); + ImGui::Text("id: %s", node->id().c_str()); if (node->parsed_args) { ImGui::Text("parsed_args (%d):", (int)node->parsed_args->size()); for (int i = 0; i < (int)node->parsed_args->size(); i++) { auto& a = (*node->parsed_args)[i]; if (auto* n = std::get_if(&a)) - ImGui::Text(" [%d] net: %s", i, n->first.c_str()); + ImGui::Text(" [%d] net: %s", i, n->first().c_str()); else if (auto* e = std::get_if(&a)) - ImGui::Text(" [%d] expr: %s", i, e->expr.c_str()); + ImGui::Text(" [%d] expr: %s", i, e->expr().c_str()); else if (auto* s = std::get_if(&a)) - ImGui::Text(" [%d] str: %s", i, s->value.c_str()); + ImGui::Text(" [%d] str: %s", i, s->value().c_str()); else if (auto* v = std::get_if(&a)) - ImGui::Text(" [%d] num: %g", i, v->value); + ImGui::Text(" [%d] num: %g", i, v->value()); } } if (node->parsed_va_args && !node->parsed_va_args->empty()) { @@ -793,19 +793,19 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, for (int i = 0; i < (int)node->parsed_va_args->size(); i++) { auto& a = (*node->parsed_va_args)[i]; if (auto* n = std::get_if(&a)) - ImGui::Text(" [%d] net: %s", i, n->first.c_str()); + ImGui::Text(" [%d] net: %s", i, n->first().c_str()); else if (auto* e = std::get_if(&a)) - ImGui::Text(" [%d] expr: %s", i, e->expr.c_str()); + ImGui::Text(" [%d] expr: %s", i, e->expr().c_str()); else if (auto* s = std::get_if(&a)) - ImGui::Text(" [%d] str: %s", i, s->value.c_str()); + ImGui::Text(" [%d] str: %s", i, s->value().c_str()); else if (auto* v = std::get_if(&a)) - ImGui::Text(" [%d] num: %g", i, v->value); + ImGui::Text(" [%d] num: %g", i, v->value()); } } if (!node->remaps.empty()) { ImGui::Text("remaps (%d):", (int)node->remaps.size()); for (int i = 0; i < (int)node->remaps.size(); i++) - ImGui::Text(" $%d -> %s", i, node->remaps[i].first.c_str()); + ImGui::Text(" $%d -> %s", i, node->remaps[i].first().c_str()); } ImGui::EndTooltip(); } From ab32c541842c963cb41b6e051141fffd2997666e Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 15:13:31 +0200 Subject: [PATCH 42/86] Refactor dirty tracking methods in GraphBuilder --- src/atto/graph_builder.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/atto/graph_builder.h b/src/atto/graph_builder.h index 3a19867..d6d8d0e 100644 --- a/src/atto/graph_builder.h +++ b/src/atto/graph_builder.h @@ -224,7 +224,7 @@ struct GraphBuilder : std::enable_shared_from_this { // Dirty tracking void mark_dirty() { dirty_ = true; } - bool was_dirty() { bool d = dirty_; dirty_ = false; return d; } + bool is_dirty() { return dirty_; } private: bool dirty_ = false; From 43c99c2310e0c9ab56938b67be87aa6152ee73b3 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 15:17:27 +0200 Subject: [PATCH 43/86] editor2: ditry state visualization --- src/attoflow/editor.cpp | 9 +++++++-- src/attoflow/editor2.h | 3 +-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/attoflow/editor.cpp b/src/attoflow/editor.cpp index 7b945a9..dc5bbed 100644 --- a/src/attoflow/editor.cpp +++ b/src/attoflow/editor.cpp @@ -861,8 +861,13 @@ void FlowEditorWindow::draw() { // --- Tab bar --- if (ImGui::BeginTabBar("##atto_tabs")) { for (int i = 0; i < (int)tabs_.size(); i++) { - std::string label = tabs_[i].tab_name; - if (tabs_[i].dirty) label += "*"; + std::string label = tabs_[i].use_editor2 + ? tabs_[i].editor2.tab_name() + : tabs_[i].tab_name; + bool tab_dirty = tabs_[i].use_editor2 + ? tabs_[i].editor2.is_dirty() + : tabs_[i].dirty; + if (tab_dirty) label += "*"; label += "###tab" + std::to_string(i); bool open = true; ImGuiTabItemFlags flags = (i == active_tab_) ? ImGuiTabItemFlags_SetSelected : 0; diff --git a/src/attoflow/editor2.h b/src/attoflow/editor2.h index 6591224..91c1218 100644 --- a/src/attoflow/editor2.h +++ b/src/attoflow/editor2.h @@ -17,7 +17,7 @@ class Editor2Pane { void draw(); bool is_loaded() const { return gb_ != nullptr; } - bool is_dirty() const { return dirty_; } + bool is_dirty() const { return gb_ && gb_->is_dirty(); } const std::string& file_path() const { return file_path_; } const std::string& tab_name() const { return tab_name_; } @@ -25,7 +25,6 @@ class Editor2Pane { std::shared_ptr gb_; std::string file_path_; std::string tab_name_; - bool dirty_ = false; // Canvas state ImVec2 canvas_offset_ = {0, 0}; From bf8c9cddcc187d96c6e87aba58c61c69156299da Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 16:21:01 +0200 Subject: [PATCH 44/86] graph rename --- src/atto/graph_builder.cpp | 12 ++++++++++++ src/atto/graph_builder.h | 3 +++ 2 files changed, 15 insertions(+) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index c577615..a85481f 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -293,6 +293,18 @@ NodeId GraphBuilder::next_id() { } } +bool GraphBuilder::rename(const BuilderEntryPtr& entry, const NodeId& new_id) { + if (!entry) return false; + if (entries.count(new_id)) return false; // collision + + const NodeId old_id = entry->id(); + entries.erase(old_id); + entry->id(new_id); + entries[new_id] = entry; + mark_dirty(); + return true; +} + // ─── Deserializer ─── BuilderResult Deserializer::parse_node( diff --git a/src/atto/graph_builder.h b/src/atto/graph_builder.h index d6d8d0e..c0e7a13 100644 --- a/src/atto/graph_builder.h +++ b/src/atto/graph_builder.h @@ -222,6 +222,9 @@ struct GraphBuilder : std::enable_shared_from_this { NodeId next_id(); + // Rename an entry (node or net). Returns false if new_id already exists. + bool rename(const BuilderEntryPtr& entry, const NodeId& new_id); + // Dirty tracking void mark_dirty() { dirty_ = true; } bool is_dirty() { return dirty_; } From 910408257a64d55874e875a2b7642603dae4a1f4 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 16:25:53 +0200 Subject: [PATCH 45/86] wire hovers --- src/attoflow/editor2.cpp | 75 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 5f00d74..af85b7e 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -29,6 +29,7 @@ static struct { // Hit testing float pin_hit_radius_mul = 2.5f; + float wire_hit_threshold = 60.0f; // Canvas colors ImU32 col_bg = IM_COL32(30, 30, 40, 255); @@ -66,6 +67,28 @@ static struct { // ─── Helpers ─── +static float point_to_bezier_dist(ImVec2 p, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3) { + float min_d2 = 1e18f; + for (int i = 0; i <= 20; i++) { + float t = i / 20.0f; + float u = 1.0f - t; + float x = u*u*u*p0.x + 3*u*u*t*p1.x + 3*u*t*t*p2.x + t*t*t*p3.x; + float y = u*u*u*p0.y + 3*u*u*t*p1.y + 3*u*t*t*p2.y + t*t*t*p3.y; + float dx = p.x - x, dy = p.y - y; + float d2 = dx*dx + dy*dy; + if (d2 < min_d2) min_d2 = d2; + } + return std::sqrt(min_d2); +} + +// Stored wire info for hover hit-testing +struct WireInfo { + ImVec2 p0, p1, p2, p3; // bezier control points + NodeId src_id, dst_id; // source and destination node IDs + NodeId net_id; // net name + bool is_lambda = false; +}; + static inline ImVec2 v2add(ImVec2 a, ImVec2 b) { return {a.x + b.x, a.y + b.y}; } static inline ImVec2 v2sub(ImVec2 a, ImVec2 b) { return {a.x - b.x, a.y - b.y}; } static inline ImVec2 v2mul(ImVec2 a, float s) { return {a.x * s, a.y * s}; } @@ -264,6 +287,8 @@ void Editor2Pane::draw() { } // Draw wires by iterating each node's inputs/outputs/remaps + std::vector drawn_wires; // collect for hover testing + for (auto& [dst_id, dst_entry] : gb_->entries) { auto dst_node = dst_entry->as_Node(); if (!dst_node) continue; @@ -306,13 +331,14 @@ void Editor2Pane::draw() { ImVec2 to = dst_layout.input_pin_pos(dst_pin); float th = S.wire_thickness * canvas_zoom_; - ImVec2 from; + ImVec2 from, cp1, cp2; + ImU32 wire_col; if (is_lambda) { from = src_layout.lambda_grab_pos(); float dx = std::max(std::abs(to.x - from.x) * 0.5f, 30.0f * canvas_zoom_); float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); - ImU32 col = S.col_wire_lambda; - dl->AddBezierCubic(from, {from.x - dx, from.y}, {to.x, to.y - dy}, to, col, th); + cp1 = {from.x - dx, from.y}; cp2 = {to.x, to.y - dy}; + wire_col = S.col_wire_lambda; } else { bool is_side_bang = src_nt && src_nt->is_flow() && source_pin < (src_nt->num_outputs) && @@ -322,16 +348,18 @@ void Editor2Pane::draw() { from = {src_layout.pos.x + src_layout.width, src_layout.pos.y + src_layout.height * 0.5f}; float dx = std::max(std::abs(to.x - from.x) * 0.5f, 30.0f * canvas_zoom_); float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); - ImU32 col = named ? S.col_wire_named : S.col_wire; - dl->AddBezierCubic(from, {from.x + dx, from.y}, {to.x, to.y - dy}, to, col, th); + cp1 = {from.x + dx, from.y}; cp2 = {to.x, to.y - dy}; } else { from = src_layout.output_pin_pos(source_pin); - ImU32 col = named ? S.col_wire_named : S.col_wire; float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); - dl->AddBezierCubic(from, {from.x, from.y + dy}, {to.x, to.y - dy}, to, col, th); + cp1 = {from.x, from.y + dy}; cp2 = {to.x, to.y - dy}; } + wire_col = named ? S.col_wire_named : S.col_wire; } + dl->AddBezierCubic(from, cp1, cp2, to, wire_col, th); + drawn_wires.push_back({from, cp1, cp2, to, src_node->id(), dst_id, net_id, is_lambda}); + // Label for named nets if (named) { float font_size = ImGui::GetFontSize() * canvas_zoom_ * 0.8f; @@ -374,6 +402,39 @@ void Editor2Pane::draw() { } } + // Wire hover: highlight + tooltip + if (canvas_hovered) { + ImVec2 mouse = ImGui::GetIO().MousePos; + float wire_hit_threshold = S.wire_hit_threshold * canvas_zoom_; + const WireInfo* hovered_wire = nullptr; + float best_dist = wire_hit_threshold; + for (auto& w : drawn_wires) { + float d = point_to_bezier_dist(mouse, w.p0, w.p1, w.p2, w.p3); + if (d < best_dist) { + best_dist = d; + hovered_wire = &w; + } + } + if (hovered_wire) { + // Redraw the wire brighter as highlight + float th = (S.wire_thickness + 2.0f) * canvas_zoom_; + ImU32 col = S.col_pin_hover; + dl->AddBezierCubic(hovered_wire->p0, hovered_wire->p1, hovered_wire->p2, hovered_wire->p3, col, th); + + // Tooltip + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + if (hovered_wire->is_lambda) { + ImGui::Text("lambda: %s", hovered_wire->src_id.c_str()); + } else { + ImGui::Text("net: %s", hovered_wire->net_id.c_str()); + } + ImGui::Text("src: %s", hovered_wire->src_id.c_str()); + ImGui::Text("dst: %s", hovered_wire->dst_id.c_str()); + ImGui::EndTooltip(); + } + } + dl->PopClipRect(); // ─── Node dragging with left mouse ─── From 87e083e1f7655944f18da538e86954461dca81a7 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 16:35:19 +0200 Subject: [PATCH 46/86] node selection. --- src/attoflow/editor2.cpp | 123 ++++++++++++++++++++------------------- src/attoflow/editor2.h | 3 +- 2 files changed, 65 insertions(+), 61 deletions(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index af85b7e..d9814ab 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -30,6 +30,7 @@ static struct { // Hit testing float pin_hit_radius_mul = 2.5f; float wire_hit_threshold = 60.0f; + float dismiss_radius = 20.0f; // click within this of empty space/wire dismisses selection // Canvas colors ImU32 col_bg = IM_COL32(30, 30, 40, 255); @@ -437,88 +438,92 @@ void Editor2Pane::draw() { dl->PopClipRect(); - // ─── Node dragging with left mouse ─── + // ─── Selection + dragging with left mouse ─── if (canvas_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { - // Hit test: find node under mouse ImVec2 mouse = ImGui::GetIO().MousePos; - dragging_node_.clear(); - // Iterate in reverse so topmost (last drawn) is hit first + + // Hit test: find node under mouse (reverse order = topmost first) + FlowNodeBuilderPtr hit_node = nullptr; for (auto it = gb_->entries.rbegin(); it != gb_->entries.rend(); ++it) { auto node = it->second->as_Node(); if (!node) continue; auto layout = compute_node_layout(node, canvas_origin, canvas_zoom_); if (mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height) { - dragging_node_ = it->first; + hit_node = node; + break; + } + } + + if (hit_node) { + // If node is not already in selection, replace selection with just this node + if (!selected_nodes_.count(hit_node)) { selected_nodes_.clear(); - selected_nodes_.insert(dragging_node_); - dragging_started_ = true; - // Check if already overlapping at drag start - drag_was_overlapping_ = false; - { - auto dl2 = compute_node_layout(node, {0,0}, 1.0f); - float pad = S.node_height * 0.5f; - for (auto& [oid, oe] : gb_->entries) { - if (oid == dragging_node_) continue; - auto on = oe->as_Node(); - if (!on) continue; - auto ol = compute_node_layout(on, {0,0}, 1.0f); - if (node->position.x < on->position.x - pad + ol.width + pad * 2 && - node->position.x + dl2.width > on->position.x - pad && - node->position.y < on->position.y - pad + ol.height + pad * 2 && - node->position.y + dl2.height > on->position.y - pad) { - drag_was_overlapping_ = true; - break; - } + selected_nodes_.insert(hit_node); + } + dragging_started_ = true; + + // Check if any selected node is already overlapping at drag start + drag_was_overlapping_ = false; + float pad = S.node_height * 0.5f; + for (auto& sel : selected_nodes_) { + auto sel_layout = compute_node_layout(sel, {0,0}, 1.0f); + for (auto& [oid, oe] : gb_->entries) { + auto on = oe->as_Node(); + if (!on || selected_nodes_.count(on)) continue; + auto ol = compute_node_layout(on, {0,0}, 1.0f); + if (sel->position.x < on->position.x - pad + ol.width + pad * 2 && + sel->position.x + sel_layout.width > on->position.x - pad && + sel->position.y < on->position.y - pad + ol.height + pad * 2 && + sel->position.y + sel_layout.height > on->position.y - pad) { + drag_was_overlapping_ = true; + break; } } - break; + if (drag_was_overlapping_) break; } - } - if (dragging_node_.empty()) { + } else { + // Clicked on empty space or wire — dismiss selection selected_nodes_.clear(); } } - if (dragging_started_ && ImGui::IsMouseDragging(ImGuiMouseButton_Left) && !dragging_node_.empty()) { - auto it = gb_->entries.find(dragging_node_); - auto drag_node = (it != gb_->entries.end()) ? it->second->as_Node() : nullptr; - if (drag_node) { - ImVec2 delta = ImGui::GetIO().MouseDelta; - float new_x = drag_node->position.x + delta.x / canvas_zoom_; - float new_y = drag_node->position.y + delta.y / canvas_zoom_; - - // Compute proposed layout - auto proposed = compute_node_layout(drag_node, {0,0}, 1.0f); - float pw = proposed.width; - float ph = proposed.height; - - // Check overlap with all other nodes (0.5em padding) - // If the node was already overlapping at drag start, allow free movement - bool blocked = false; - if (!drag_was_overlapping_) { - float pad = S.node_height * 0.5f; - for (auto& [other_id, other_entry] : gb_->entries) { - if (other_id == dragging_node_) continue; - auto other_node = other_entry->as_Node(); - if (!other_node) continue; - auto other_layout = compute_node_layout(other_node, {0,0}, 1.0f); - float ox = other_node->position.x - pad, oy = other_node->position.y - pad; - float ow = other_layout.width + pad * 2, oh = other_layout.height + pad * 2; - if (new_x < ox + ow && new_x + pw > ox && - new_y < oy + oh && new_y + ph > oy) { + + // Drag all selected nodes + if (dragging_started_ && ImGui::IsMouseDragging(ImGuiMouseButton_Left) && !selected_nodes_.empty()) { + ImVec2 delta = ImGui::GetIO().MouseDelta; + float dx = delta.x / canvas_zoom_; + float dy = delta.y / canvas_zoom_; + + // Check overlap for all selected nodes against all non-selected nodes + bool blocked = false; + if (!drag_was_overlapping_) { + float pad = S.node_height * 0.5f; + for (auto& sel : selected_nodes_) { + auto sel_layout = compute_node_layout(sel, {0,0}, 1.0f); + float nx = sel->position.x + dx, ny = sel->position.y + dy; + for (auto& [oid, oe] : gb_->entries) { + auto on = oe->as_Node(); + if (!on || selected_nodes_.count(on)) continue; + auto ol = compute_node_layout(on, {0,0}, 1.0f); + float ox = on->position.x - pad, oy = on->position.y - pad; + float ow = ol.width + pad * 2, oh = ol.height + pad * 2; + if (nx < ox + ow && nx + sel_layout.width > ox && + ny < oy + oh && ny + sel_layout.height > oy) { blocked = true; break; } } + if (blocked) break; } - if (!blocked) { - drag_node->position.x = new_x; - drag_node->position.y = new_y; + } + if (!blocked) { + for (auto& sel : selected_nodes_) { + sel->position.x += dx; + sel->position.y += dy; } } } if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { - dragging_node_.clear(); dragging_started_ = false; } @@ -595,7 +600,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, std::string args = node->args_str(); if (!args.empty()) display += " " + args; - bool selected = selected_nodes_.count(node->id()); + bool selected = selected_nodes_.count(node); bool has_error = !node->error.empty(); ImU32 col = has_error ? S.col_node_err : (selected ? S.col_node_sel : S.col_node); diff --git a/src/attoflow/editor2.h b/src/attoflow/editor2.h index 91c1218..ef4ce03 100644 --- a/src/attoflow/editor2.h +++ b/src/attoflow/editor2.h @@ -31,8 +31,7 @@ class Editor2Pane { float canvas_zoom_ = 1.0f; // Interaction state - std::set selected_nodes_; - NodeId dragging_node_; // node being dragged (empty = none) + std::set selected_nodes_; bool dragging_started_ = false; bool drag_was_overlapping_ = false; // true if node was overlapping when drag began int editing_link_id_ = -1; // not used yet, placeholder From 297bf7bdda8cbe4a802cbdbcb768292c29bf8a1f Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 16:41:26 +0200 Subject: [PATCH 47/86] hover_item_ initial work --- src/attoflow/editor2.cpp | 102 ++++++++++++++++++++++----------------- src/attoflow/editor2.h | 2 + 2 files changed, 61 insertions(+), 43 deletions(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index d9814ab..6833c82 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -403,63 +403,77 @@ void Editor2Pane::draw() { } } - // Wire hover: highlight + tooltip - if (canvas_hovered) { - ImVec2 mouse = ImGui::GetIO().MousePos; - float wire_hit_threshold = S.wire_hit_threshold * canvas_zoom_; - const WireInfo* hovered_wire = nullptr; - float best_dist = wire_hit_threshold; - for (auto& w : drawn_wires) { - float d = point_to_bezier_dist(mouse, w.p0, w.p1, w.p2, w.p3); - if (d < best_dist) { - best_dist = d; - hovered_wire = &w; - } - } - if (hovered_wire) { - // Redraw the wire brighter as highlight - float th = (S.wire_thickness + 2.0f) * canvas_zoom_; - ImU32 col = S.col_pin_hover; - dl->AddBezierCubic(hovered_wire->p0, hovered_wire->p1, hovered_wire->p2, hovered_wire->p3, col, th); - - // Tooltip - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(S.tooltip_scale); - if (hovered_wire->is_lambda) { - ImGui::Text("lambda: %s", hovered_wire->src_id.c_str()); - } else { - ImGui::Text("net: %s", hovered_wire->net_id.c_str()); - } - ImGui::Text("src: %s", hovered_wire->src_id.c_str()); - ImGui::Text("dst: %s", hovered_wire->dst_id.c_str()); - ImGui::EndTooltip(); - } - } - dl->PopClipRect(); - // ─── Selection + dragging with left mouse ─── - if (canvas_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + // ─── Determine hover item (node or wire) ─── + hover_item_.reset(); + if (canvas_hovered) { ImVec2 mouse = ImGui::GetIO().MousePos; - // Hit test: find node under mouse (reverse order = topmost first) - FlowNodeBuilderPtr hit_node = nullptr; + // Check nodes first (they have priority over wires) for (auto it = gb_->entries.rbegin(); it != gb_->entries.rend(); ++it) { auto node = it->second->as_Node(); if (!node) continue; auto layout = compute_node_layout(node, canvas_origin, canvas_zoom_); if (mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height) { - hit_node = node; + hover_item_ = node; break; } } - if (hit_node) { - // If node is not already in selection, replace selection with just this node - if (!selected_nodes_.count(hit_node)) { + // If no node hit, check wires + if (hover_item_.expired()) { + float wire_thresh = S.wire_hit_threshold * canvas_zoom_; + float best_dist = wire_thresh; + BuilderEntryPtr best_wire; + const WireInfo* best_wire_info = nullptr; + for (auto& w : drawn_wires) { + float d = point_to_bezier_dist(mouse, w.p0, w.p1, w.p2, w.p3); + if (d < best_dist) { + best_dist = d; + auto entry = gb_->find(w.net_id); + if (entry) { best_wire = entry; best_wire_info = &w; } + } + } + if (best_wire) hover_item_ = best_wire; + + // Draw wire highlight + tooltip + if (best_wire_info) { + float th = (S.wire_thickness + 2.0f) * canvas_zoom_; + dl->AddBezierCubic(best_wire_info->p0, best_wire_info->p1, + best_wire_info->p2, best_wire_info->p3, S.col_pin_hover, th); + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + if (best_wire_info->is_lambda) + ImGui::Text("lambda: %s", best_wire_info->src_id.c_str()); + else + ImGui::Text("net: %s", best_wire_info->net_id.c_str()); + ImGui::Text("src: %s", best_wire_info->src_id.c_str()); + ImGui::Text("dst: %s", best_wire_info->dst_id.c_str()); + ImGui::EndTooltip(); + } + } + } + + auto hover_locked = hover_item_.lock(); + auto hover_node = hover_locked ? hover_locked->as_Node() : nullptr; + + // ─── Selection + dragging with left mouse ─── + if (canvas_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + bool ctrl = ImGui::GetIO().KeyCtrl; + + if (ctrl && hover_node) { + // Ctrl+click: toggle node in selection + if (selected_nodes_.count(hover_node)) + selected_nodes_.erase(hover_node); + else + selected_nodes_.insert(hover_node); + } else if (hover_node) { + // Regular click on node: select if not already, start drag + if (!selected_nodes_.count(hover_node)) { selected_nodes_.clear(); - selected_nodes_.insert(hit_node); + selected_nodes_.insert(hover_node); } dragging_started_ = true; @@ -601,13 +615,15 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, if (!args.empty()) display += " " + args; bool selected = selected_nodes_.count(node); + bool hovered = (hover_item_.lock() == node); bool has_error = !node->error.empty(); ImU32 col = has_error ? S.col_node_err : (selected ? S.col_node_sel : S.col_node); dl->AddRectFilled(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, col, S.node_rounding * canvas_zoom_); dl->AddRect(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, - S.col_node_border, S.node_rounding * canvas_zoom_); + hovered ? S.col_pin_hover : S.col_node_border, S.node_rounding * canvas_zoom_, + 0, hovered ? S.highlight_thickness : 1.0f); // Text float font_size = ImGui::GetFontSize() * canvas_zoom_; diff --git a/src/attoflow/editor2.h b/src/attoflow/editor2.h index ef4ce03..9a3a17f 100644 --- a/src/attoflow/editor2.h +++ b/src/attoflow/editor2.h @@ -31,6 +31,8 @@ class Editor2Pane { float canvas_zoom_ = 1.0f; // Interaction state + BuilderEntryWeak hover_item_; // current hovered item (node or net), empty = nothing + // TODO: pins will be part of hover_item_ system (needs Pin as a graph entity or lightweight ID) std::set selected_nodes_; bool dragging_started_ = false; bool drag_was_overlapping_ = false; // true if node was overlapping when drag began From 31178ae1e6149483e888f557122af20b29c9d009 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 17:02:17 +0200 Subject: [PATCH 48/86] ArgNet2 w/ builders --- src/atto/graph_builder.cpp | 56 +++++++++++++++++------ src/atto/graph_builder.h | 93 +++++++++++++++++++++++++++----------- 2 files changed, 107 insertions(+), 42 deletions(-) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index a85481f..82621d5 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -62,20 +62,25 @@ static std::vector parse_toml_array(const std::string& val) { static void maybe_dirty(GraphBuilder* gb) { if (gb) gb->mark_dirty(); } static void maybe_dirty(const std::shared_ptr& gb) { if (gb) gb->mark_dirty(); } -void ArgNet2::id(const NodeId& v, GraphBuilder* gb) { id_ = v; maybe_dirty(gb); } -void ArgNet2::entry(std::shared_ptr v, GraphBuilder* gb) { entry_ = std::move(v); maybe_dirty(gb); } -void ArgNumber2::value(double v, GraphBuilder* gb) { value_ = v; maybe_dirty(gb); } -void ArgNumber2::is_float(bool v, GraphBuilder* gb) { is_float_ = v; maybe_dirty(gb); } -void ArgString2::value(const std::string& v, GraphBuilder* gb) { value_ = v; maybe_dirty(gb); } -void ArgExpr2::expr(const std::string& v, GraphBuilder* gb) { expr_ = v; maybe_dirty(gb); } +void ArgNet2::id(const NodeId& v) { id_ = v; maybe_dirty(owner_); } +void ArgNet2::entry(std::shared_ptr v) { entry_ = std::move(v); maybe_dirty(owner_); } +void ArgNumber2::value(double v) { value_ = v; maybe_dirty(owner_); } +void ArgNumber2::is_float(bool v) { is_float_ = v; maybe_dirty(owner_); } +void ArgString2::value(const std::string& v) { value_ = v; maybe_dirty(owner_); } +void ArgExpr2::expr(const std::string& v) { expr_ = v; maybe_dirty(owner_); } // ─── ParsedArgs2 ─── -void ParsedArgs2::push_back(FlowArg2 arg) { items_.push_back(std::move(arg)); maybe_dirty(owner); } -void ParsedArgs2::pop_back() { items_.pop_back(); maybe_dirty(owner); } -void ParsedArgs2::resize(int n) { items_.resize(n); maybe_dirty(owner); } -void ParsedArgs2::insert(std::vector::iterator pos, FlowArg2 arg) { items_.insert(pos, std::move(arg)); maybe_dirty(owner); } -void ParsedArgs2::clear() { items_.clear(); maybe_dirty(owner); } +void ParsedArgs2::push_back(FlowArg2Ptr arg) { items_.push_back(std::move(arg)); maybe_dirty(owner); } +void ParsedArgs2::pop_back() { items_.pop_back(); maybe_dirty(owner); } +void ParsedArgs2::resize(int n) { + while ((int)items_.size() < n) items_.push_back(std::make_shared()); + items_.resize(n); + maybe_dirty(owner); +} +void ParsedArgs2::insert(iterator pos, FlowArg2Ptr arg) { items_.insert(pos, std::move(arg)); maybe_dirty(owner); } +void ParsedArgs2::clear() { items_.clear(); maybe_dirty(owner); } +void ParsedArgs2::set(int i, FlowArg2Ptr arg) { items_[i] = std::move(arg); maybe_dirty(owner); } // ─── BuilderEntry ─── @@ -305,6 +310,27 @@ bool GraphBuilder::rename(const BuilderEntryPtr& entry, const NodeId& new_id) { return true; } +FlowArg2Ptr GraphBuilder::build_arg_net(NodeId id, BuilderEntryPtr entry) { + auto p = std::make_shared(ArgNet2{std::move(id), std::move(entry), shared_from_this()}); + pins_.push_back(p); + return p; +} +FlowArg2Ptr GraphBuilder::build_arg_number(double value, bool is_float) { + auto p = std::make_shared(ArgNumber2{value, is_float, shared_from_this()}); + pins_.push_back(p); + return p; +} +FlowArg2Ptr GraphBuilder::build_arg_string(std::string value) { + auto p = std::make_shared(ArgString2{std::move(value), shared_from_this()}); + pins_.push_back(p); + return p; +} +FlowArg2Ptr GraphBuilder::build_arg_expr(std::string expr) { + auto p = std::make_shared(ArgExpr2{std::move(expr), shared_from_this()}); + pins_.push_back(p); + return p; +} + // ─── Deserializer ─── BuilderResult Deserializer::parse_node( @@ -324,7 +350,7 @@ BuilderResult Deserializer::parse_node( if (args.size() != 1) throw std::invalid_argument("Label/Error node requires exactly 1 argument, got " + std::to_string(args.size())); nb.parsed_args = std::make_shared(); - nb.parsed_args->push_back(ArgString2{args[0]}); + nb.parsed_args->push_back(std::make_shared(ArgString2{args[0]})); return std::pair{id, std::move(nb)}; } @@ -362,7 +388,7 @@ FlowNodeBuilder& Deserializer::parse_or_error( auto entry = std::make_shared(gb); entry->type_id = NodeTypeID::Error; entry->parsed_args = std::make_shared(); - entry->parsed_args->push_back(ArgString2{type + " " + args_joined}); + entry->parsed_args->push_back(std::make_shared(ArgString2{type + " " + args_joined})); entry->error = error_msg; entry->id(id); gb->entries[id] = entry; @@ -675,7 +701,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { for (int i = 0; i < new_nt->total_inputs(); i++) { if (!filled[i] && new_nt->input_port(i)->kind != PortKind2::BangTrigger) { if (arg_cursor < (int)nb.parsed_args->size()) { - (*merged)[i] = std::move((*nb.parsed_args)[arg_cursor++]); + merged->set(i, std::move((*nb.parsed_args)[arg_cursor++])); filled[i] = true; } } @@ -823,7 +849,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // Fallback: try positional insertion (for nodes without merged inputs) if (!replaced) { while ((int)parent_ptr->parsed_args->size() <= arg_index) - parent_ptr->parsed_args->push_back(ArgString2{""}); + parent_ptr->parsed_args->push_back(std::make_shared(ArgString2{""})); (*parent_ptr->parsed_args)[arg_index] = (*shadow_ptr->parsed_args)[0]; } } diff --git a/src/atto/graph_builder.h b/src/atto/graph_builder.h index c0e7a13..b254937 100644 --- a/src/atto/graph_builder.h +++ b/src/atto/graph_builder.h @@ -29,64 +29,86 @@ struct NetBuilder; // ─── Arg types with dirty tracking ─── struct ArgNet2 { - ArgNet2() = default; - ArgNet2(NodeId id, std::shared_ptr entry) : id_(std::move(id)), entry_(std::move(entry)) {} + friend struct GraphBuilder; const NodeId& id() const { return id_; } - void id(const NodeId& v, GraphBuilder* gb = nullptr); + void id(const NodeId& v); const std::shared_ptr& entry() const { return entry_; } - void entry(std::shared_ptr v, GraphBuilder* gb = nullptr); + void entry(std::shared_ptr v); - // Convenience: access like old pair + // Convenience const NodeId& first() const { return id_; } const std::shared_ptr& second() const { return entry_; } + std::shared_ptr owner() const { return owner_; } + private: + ArgNet2(NodeId id, std::shared_ptr entry, + const std::shared_ptr& owner) + : id_(std::move(id)), entry_(std::move(entry)), owner_(owner) {} + NodeId id_; std::shared_ptr entry_; + std::shared_ptr owner_; }; struct ArgNumber2 { - ArgNumber2() = default; - ArgNumber2(double v, bool f) : value_(v), is_float_(f) {} + friend struct GraphBuilder; double value() const { return value_; } - void value(double v, GraphBuilder* gb = nullptr); + void value(double v); bool is_float() const { return is_float_; } - void is_float(bool v, GraphBuilder* gb = nullptr); + void is_float(bool v); + + std::shared_ptr owner() const { return owner_; } private: + ArgNumber2(double v, bool f, const std::shared_ptr& owner) + : value_(v), is_float_(f), owner_(owner) {} + double value_ = 0; bool is_float_ = false; + std::shared_ptr owner_; }; struct ArgString2 { - ArgString2() = default; - ArgString2(std::string v) : value_(std::move(v)) {} + friend struct GraphBuilder; const std::string& value() const { return value_; } - void value(const std::string& v, GraphBuilder* gb = nullptr); + void value(const std::string& v); + + std::shared_ptr owner() const { return owner_; } private: + ArgString2(std::string v, const std::shared_ptr& owner) + : value_(std::move(v)), owner_(owner) {} + std::string value_; + std::shared_ptr owner_; }; struct ArgExpr2 { - ArgExpr2() = default; - ArgExpr2(std::string e) : expr_(std::move(e)) {} + friend struct GraphBuilder; const std::string& expr() const { return expr_; } - void expr(const std::string& v, GraphBuilder* gb = nullptr); + void expr(const std::string& v); + + std::shared_ptr owner() const { return owner_; } private: + ArgExpr2(std::string e, const std::shared_ptr& owner) + : expr_(std::move(e)), owner_(owner) {} + std::string expr_; + std::shared_ptr owner_; }; using FlowArg2 = std::variant; +using FlowArg2Ptr = std::shared_ptr; -// ─── ParsedArgs2: vector wrapper with dirty tracking ─── +// ─── ParsedArgs2: vector of shared_ptr with dirty tracking ─── struct ParsedArgs2 { int rewrite_input_count = 0; @@ -94,27 +116,35 @@ struct ParsedArgs2 { // Read access bool empty() const { return items_.empty(); } int size() const { return (int)items_.size(); } - FlowArg2& operator[](int i) { return items_[i]; } - const FlowArg2& operator[](int i) const { return items_[i]; } - auto begin() { return items_.begin(); } - auto end() { return items_.end(); } - auto begin() const { return items_.begin(); } - auto end() const { return items_.end(); } - FlowArg2& back() { return items_.back(); } - const FlowArg2& back() const { return items_.back(); } + FlowArg2Ptr at(int i) { return items_[i]; } + FlowArg2Ptr at(int i) const { return items_[i]; } + FlowArg2& operator[](int i) { return *items_[i]; } + const FlowArg2& operator[](int i) const { return *items_[i]; } + + using iterator = std::vector::iterator; + using const_iterator = std::vector::const_iterator; + iterator begin() { return items_.begin(); } + iterator end() { return items_.end(); } + const_iterator begin() const { return items_.begin(); } + const_iterator end() const { return items_.end(); } + FlowArg2Ptr back() { return items_.back(); } + FlowArg2Ptr back() const { return items_.back(); } // Write access (marks dirty) - void push_back(FlowArg2 arg); + void push_back(FlowArg2Ptr arg); void pop_back(); void resize(int n); - void insert(typename std::vector::iterator pos, FlowArg2 arg); + void insert(iterator pos, FlowArg2Ptr arg); void clear(); + // Set item at index (marks dirty) + void set(int i, FlowArg2Ptr arg); + // Owner std::shared_ptr owner; private: - std::vector items_; + std::vector items_; }; // ─── BuilderEntry base ─── @@ -225,12 +255,21 @@ struct GraphBuilder : std::enable_shared_from_this { // Rename an entry (node or net). Returns false if new_id already exists. bool rename(const BuilderEntryPtr& entry, const NodeId& new_id); + // Arg factories — all pins are tracked in pins_ + FlowArg2Ptr build_arg_net(NodeId id, BuilderEntryPtr entry); + FlowArg2Ptr build_arg_number(double value, bool is_float); + FlowArg2Ptr build_arg_string(std::string value); + FlowArg2Ptr build_arg_expr(std::string expr); + + const std::vector& pins() const { return pins_; } + // Dirty tracking void mark_dirty() { dirty_ = true; } bool is_dirty() { return dirty_; } private: bool dirty_ = false; + std::vector pins_; }; // ─── Parse/reconstruct helpers ─── From 181216795ac4b01f81a278a3be7ebb8663eb8a70 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 17:07:13 +0200 Subject: [PATCH 49/86] towards first class pins --- src/atto/graph_builder.cpp | 95 +++++++++++++++++++++----------- src/atto/graph_builder.h | 109 +++++++++++++++++++++++++------------ 2 files changed, 135 insertions(+), 69 deletions(-) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index 82621d5..b8f6427 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -57,24 +57,55 @@ static std::vector parse_toml_array(const std::string& val) { return result; } -// ─── Dirty-tracked setters ─── +// ─── FlowArg2 base ─── -static void maybe_dirty(GraphBuilder* gb) { if (gb) gb->mark_dirty(); } static void maybe_dirty(const std::shared_ptr& gb) { if (gb) gb->mark_dirty(); } -void ArgNet2::id(const NodeId& v) { id_ = v; maybe_dirty(owner_); } -void ArgNet2::entry(std::shared_ptr v) { entry_ = std::move(v); maybe_dirty(owner_); } -void ArgNumber2::value(double v) { value_ = v; maybe_dirty(owner_); } -void ArgNumber2::is_float(bool v) { is_float_ = v; maybe_dirty(owner_); } -void ArgString2::value(const std::string& v) { value_ = v; maybe_dirty(owner_); } -void ArgExpr2::expr(const std::string& v) { expr_ = v; maybe_dirty(owner_); } +void FlowArg2::mark_dirty() { maybe_dirty(owner_); } + +std::shared_ptr FlowArg2::as_net() { + return kind_ == ArgKind::Net ? std::dynamic_pointer_cast(shared_from_this()) : nullptr; +} +std::shared_ptr FlowArg2::as_number() { + return kind_ == ArgKind::Number ? std::dynamic_pointer_cast(shared_from_this()) : nullptr; +} +std::shared_ptr FlowArg2::as_string() { + return kind_ == ArgKind::String ? std::dynamic_pointer_cast(shared_from_this()) : nullptr; +} +std::shared_ptr FlowArg2::as_expr() { + return kind_ == ArgKind::Expr ? std::dynamic_pointer_cast(shared_from_this()) : nullptr; +} + +std::string FlowArg2::name() const { + // Build: "node_name.port_name" or "node_name.va_name[idx]" + std::string prefix; + auto n = node_.lock(); + if (n) prefix = n->id(); + else prefix = "$empty"; + + if (port_) { + // TODO: Check if this is a va_arg by looking at the node's va_args + // For now, just use port name + return prefix + "." + port_->name; + } + return prefix + ".?"; +} + +// ─── Dirty-tracked setters ─── + +void ArgNet2::net_id(const NodeId& v) { net_id_ = v; mark_dirty(); } +void ArgNet2::entry(std::shared_ptr v) { entry_ = std::move(v); mark_dirty(); } +void ArgNumber2::value(double v) { value_ = v; mark_dirty(); } +void ArgNumber2::is_float(bool v) { is_float_ = v; mark_dirty(); } +void ArgString2::value(const std::string& v) { value_ = v; mark_dirty(); } +void ArgExpr2::expr(const std::string& v) { expr_ = v; mark_dirty(); } // ─── ParsedArgs2 ─── void ParsedArgs2::push_back(FlowArg2Ptr arg) { items_.push_back(std::move(arg)); maybe_dirty(owner); } void ParsedArgs2::pop_back() { items_.pop_back(); maybe_dirty(owner); } void ParsedArgs2::resize(int n) { - while ((int)items_.size() < n) items_.push_back(std::make_shared()); + // Can't create default FlowArg2 (abstract), just truncate if shrinking items_.resize(n); maybe_dirty(owner); } @@ -112,18 +143,18 @@ void NetBuilder::validate() const { // ─── v2 parse/reconstruct ─── -static FlowArg2 parse_token_v2(GraphBuilder& gb, const std::string& tok) { - if (tok.empty()) return ArgString2{""}; +static FlowArg2Ptr parse_token_v2(GraphBuilder& gb, const std::string& tok) { + if (tok.empty()) return gb.build_arg_string(""); // Net reference: $name (non-numeric) if (tok[0] == '$' && tok.size() >= 2 && !std::isdigit(tok[1])) { auto [id, entry] = gb.find_or_create_net(tok, false); - return ArgNet2{NodeId(id), entry}; + return gb.build_arg_net(NodeId(id), entry); } // String literal if (tok.front() == '"' && tok.back() == '"' && tok.size() >= 2) { - return ArgString2{tok.substr(1, tok.size() - 2)}; + return gb.build_arg_string(tok.substr(1, tok.size() - 2)); } // Number @@ -137,11 +168,11 @@ static FlowArg2 parse_token_v2(GraphBuilder& gb, const std::string& tok) { if (c < '0' || c > '9') { is_number = false; break; } } if (is_number && !tok.empty()) { - return ArgNumber2{std::stod(tok), is_float}; + return gb.build_arg_number(std::stod(tok), is_float); } // Expression (anything else) - return ArgExpr2{tok}; + return gb.build_arg_expr(tok); } ParseResult parse_args_v2(const std::shared_ptr& gb, @@ -189,22 +220,20 @@ ParseResult parse_args_v2(const std::shared_ptr& gb, std::string reconstruct_args_str(const ParsedArgs2& args) { std::string result; for (auto& a : args) { + if (!a) continue; if (!result.empty()) result += " "; - std::visit([&](auto& v) { - using T = std::decay_t; - if constexpr (std::is_same_v) result += v.first(); - else if constexpr (std::is_same_v) { - if (v.is_float()) { - char buf[64]; - snprintf(buf, sizeof(buf), "%g", v.value()); - result += buf; - } else { - result += std::to_string((long long)v.value()); - } + if (auto n = a->as_net()) result += n->first(); + else if (auto num = a->as_number()) { + if (num->is_float()) { + char buf[64]; + snprintf(buf, sizeof(buf), "%g", num->value()); + result += buf; + } else { + result += std::to_string((long long)num->value()); } - else if constexpr (std::is_same_v) result += "\"" + v.value() + "\""; - else if constexpr (std::is_same_v) result += v.expr(); - }, a); + } + else if (auto s = a->as_string()) result += "\"" + s->value() + "\""; + else if (auto e = a->as_expr()) result += e->expr(); } return result; } @@ -311,22 +340,22 @@ bool GraphBuilder::rename(const BuilderEntryPtr& entry, const NodeId& new_id) { } FlowArg2Ptr GraphBuilder::build_arg_net(NodeId id, BuilderEntryPtr entry) { - auto p = std::make_shared(ArgNet2{std::move(id), std::move(entry), shared_from_this()}); + auto p = std::shared_ptr(new ArgNet2{std::move(id), std::move(entry), shared_from_this()}); pins_.push_back(p); return p; } FlowArg2Ptr GraphBuilder::build_arg_number(double value, bool is_float) { - auto p = std::make_shared(ArgNumber2{value, is_float, shared_from_this()}); + auto p = std::shared_ptr(new ArgNumber2{value, is_float, shared_from_this()}); pins_.push_back(p); return p; } FlowArg2Ptr GraphBuilder::build_arg_string(std::string value) { - auto p = std::make_shared(ArgString2{std::move(value), shared_from_this()}); + auto p = std::shared_ptr(new ArgString2{std::move(value), shared_from_this()}); pins_.push_back(p); return p; } FlowArg2Ptr GraphBuilder::build_arg_expr(std::string expr) { - auto p = std::make_shared(ArgExpr2{std::move(expr), shared_from_this()}); + auto p = std::shared_ptr(new ArgExpr2{std::move(expr), shared_from_this()}); pins_.push_back(p); return p; } diff --git a/src/atto/graph_builder.h b/src/atto/graph_builder.h index b254937..56141a0 100644 --- a/src/atto/graph_builder.h +++ b/src/atto/graph_builder.h @@ -26,34 +26,85 @@ struct GraphBuilder; struct FlowNodeBuilder; struct NetBuilder; -// ─── Arg types with dirty tracking ─── +// ─── FlowArg2: base class for all pin/arg types ─── -struct ArgNet2 { +struct PortDesc2; // forward + +enum class ArgKind : uint8_t { Net, Number, String, Expr }; + +struct ArgNet2; +struct ArgNumber2; +struct ArgString2; +struct ArgExpr2; + +struct FlowArg2 : std::enable_shared_from_this { friend struct GraphBuilder; + virtual ~FlowArg2() = default; - const NodeId& id() const { return id_; } - void id(const NodeId& v); + ArgKind kind() const { return kind_; } + bool is(ArgKind k) const { return kind_ == k; } + + std::shared_ptr as_net(); + std::shared_ptr as_number(); + std::shared_ptr as_string(); + std::shared_ptr as_expr(); + + // Context: which node/wire/port this arg belongs to + std::shared_ptr node() const { return node_.lock(); } + void node(const std::shared_ptr& n) { node_ = n; } + + std::shared_ptr wire() const { return wire_.lock(); } + void wire(const std::shared_ptr& w) { wire_ = w; } + + const PortDesc2* port() const { return port_; } + void port(const PortDesc2* p) { port_ = p; } + + std::shared_ptr owner() const { return owner_; } + + // Computed name: "node.port_name" or "node.va_name[idx]" etc. + std::string name() const; + +protected: + FlowArg2(ArgKind kind, const std::shared_ptr& owner) + : kind_(kind), owner_(owner) {} + + void mark_dirty(); + +private: + ArgKind kind_; + std::shared_ptr owner_; + std::weak_ptr node_; + std::weak_ptr wire_; + const PortDesc2* port_ = nullptr; +}; + +using FlowArg2Ptr = std::shared_ptr; + +// ─── Concrete arg types ─── + +struct ArgNet2 : FlowArg2 { + friend struct GraphBuilder; + + const NodeId& net_id() const { return net_id_; } + void net_id(const NodeId& v); const std::shared_ptr& entry() const { return entry_; } void entry(std::shared_ptr v); - // Convenience - const NodeId& first() const { return id_; } + // Convenience aliases + const NodeId& first() const { return net_id_; } const std::shared_ptr& second() const { return entry_; } - std::shared_ptr owner() const { return owner_; } - private: ArgNet2(NodeId id, std::shared_ptr entry, const std::shared_ptr& owner) - : id_(std::move(id)), entry_(std::move(entry)), owner_(owner) {} + : FlowArg2(ArgKind::Net, owner), net_id_(std::move(id)), entry_(std::move(entry)) {} - NodeId id_; + NodeId net_id_; std::shared_ptr entry_; - std::shared_ptr owner_; }; -struct ArgNumber2 { +struct ArgNumber2 : FlowArg2 { friend struct GraphBuilder; double value() const { return value_; } @@ -62,53 +113,41 @@ struct ArgNumber2 { bool is_float() const { return is_float_; } void is_float(bool v); - std::shared_ptr owner() const { return owner_; } - private: ArgNumber2(double v, bool f, const std::shared_ptr& owner) - : value_(v), is_float_(f), owner_(owner) {} + : FlowArg2(ArgKind::Number, owner), value_(v), is_float_(f) {} double value_ = 0; bool is_float_ = false; - std::shared_ptr owner_; }; -struct ArgString2 { +struct ArgString2 : FlowArg2 { friend struct GraphBuilder; const std::string& value() const { return value_; } void value(const std::string& v); - std::shared_ptr owner() const { return owner_; } - private: ArgString2(std::string v, const std::shared_ptr& owner) - : value_(std::move(v)), owner_(owner) {} + : FlowArg2(ArgKind::String, owner), value_(std::move(v)) {} std::string value_; - std::shared_ptr owner_; }; -struct ArgExpr2 { +struct ArgExpr2 : FlowArg2 { friend struct GraphBuilder; const std::string& expr() const { return expr_; } void expr(const std::string& v); - std::shared_ptr owner() const { return owner_; } - private: ArgExpr2(std::string e, const std::shared_ptr& owner) - : expr_(std::move(e)), owner_(owner) {} + : FlowArg2(ArgKind::Expr, owner), expr_(std::move(e)) {} std::string expr_; - std::shared_ptr owner_; }; -using FlowArg2 = std::variant; -using FlowArg2Ptr = std::shared_ptr; - -// ─── ParsedArgs2: vector of shared_ptr with dirty tracking ─── +// ─── ParsedArgs2: vector of FlowArg2Ptr with dirty tracking ─── struct ParsedArgs2 { int rewrite_input_count = 0; @@ -116,10 +155,8 @@ struct ParsedArgs2 { // Read access bool empty() const { return items_.empty(); } int size() const { return (int)items_.size(); } - FlowArg2Ptr at(int i) { return items_[i]; } - FlowArg2Ptr at(int i) const { return items_[i]; } - FlowArg2& operator[](int i) { return *items_[i]; } - const FlowArg2& operator[](int i) const { return *items_[i]; } + FlowArg2Ptr operator[](int i) { return items_[i]; } + FlowArg2Ptr operator[](int i) const { return items_[i]; } using iterator = std::vector::iterator; using const_iterator = std::vector::const_iterator; @@ -209,8 +246,8 @@ using NetBuilderPtr = std::shared_ptr; // ─── FlowNodeBuilder ─── -using Remaps = std::vector; -using Outputs = std::vector; +using Remaps = std::vector; +using Outputs = std::vector; struct FlowNodeBuilder: BuilderEntry { FlowNodeBuilder(const std::shared_ptr& owner = nullptr): BuilderEntry(IdCategory::Node, owner) { } From 231d04cf610aff19f9106f2cde8b416d74272497 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 17:14:44 +0200 Subject: [PATCH 50/86] graph_builder rework --- src/atto/graph_builder.cpp | 95 ++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 49 deletions(-) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index b8f6427..c012771 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -379,7 +379,7 @@ BuilderResult Deserializer::parse_node( if (args.size() != 1) throw std::invalid_argument("Label/Error node requires exactly 1 argument, got " + std::to_string(args.size())); nb.parsed_args = std::make_shared(); - nb.parsed_args->push_back(std::make_shared(ArgString2{args[0]})); + nb.parsed_args->push_back(gb->build_arg_string(args[0])); return std::pair{id, std::move(nb)}; } @@ -417,7 +417,7 @@ FlowNodeBuilder& Deserializer::parse_or_error( auto entry = std::make_shared(gb); entry->type_id = NodeTypeID::Error; entry->parsed_args = std::make_shared(); - entry->parsed_args->push_back(std::make_shared(ArgString2{type + " " + args_joined})); + entry->parsed_args->push_back(gb->build_arg_string(type + " " + args_joined)); entry->error = error_msg; entry->id(id); gb->entries[id] = entry; @@ -480,11 +480,11 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { int old_num_nexts = old_nt ? old_nt->num_nexts : 0; // Helper: wire a net and return ArgNet2 - auto wire_output = [&](const std::string& net_name) -> ArgNet2 { + auto wire_output = [&](const std::string& net_name) -> FlowArg2Ptr { auto [resolved, net_ptr] = gb->find_or_create_net(net_name, true); if (auto net = net_ptr ? net_ptr->as_Net() : nullptr) net->source(node_entry); - return {resolved, net_ptr}; + return gb->build_arg_net(resolved, net_ptr); }; // Filter out empty and -as_lambda entries, wire all nets @@ -505,14 +505,14 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // (it's implicit from NodeKind2::Flow) if (!is_post_bang) { while ((int)nb.outputs.size() <= i) - nb.outputs.push_back({"$unconnected", gb->find("$unconnected")}); + nb.outputs.push_back(gb->build_arg_net("$unconnected", gb->find("$unconnected"))); nb.outputs[i] = std::move(arg); } } } else { // Name-based mapping for non-expr nodes int old_num_outs = old_nt ? old_nt->outputs : 0; - std::map out_net_map; + std::map out_net_map; for (int i = 0; i < (int)cur_outputs.size(); i++) { auto& net_name = cur_outputs[i]; @@ -550,9 +550,9 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if (it2 != out_net_map.end()) nb.outputs[i] = std::move(it2->second); else - nb.outputs[i] = {"$unconnected", unconnected}; + nb.outputs[i] = gb->build_arg_net("$unconnected", unconnected); } else { - nb.outputs[i] = {"$unconnected", unconnected}; + nb.outputs[i] = gb->build_arg_net("$unconnected", unconnected); } } } @@ -567,10 +567,10 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { bool args_are_type = is_any_of(nb.type_id, NodeTypeID::Cast, NodeTypeID::New); // Helper: resolve net/node name to ArgNet2 and register destination - auto resolve_net = [&](const std::string& net_name) -> ArgNet2 { + auto resolve_net = [&](const std::string& net_name) -> FlowArg2Ptr { if (net_name.empty()) { auto [resolved, ptr] = gb->find_or_create_net("$unconnected"); - return {resolved, ptr}; + return gb->build_arg_net(resolved, ptr); } // Strip -as_lambda suffix → resolve to node entry directly std::string resolved_name = net_name; @@ -581,15 +581,14 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // Try finding as any entry (node or net) auto ptr = gb->find(resolved_name); if (ptr) { - // If it's a net, register as destination if (auto net = ptr->as_Net()) net->destinations().push_back(node_entry); - return {resolved_name, ptr}; + return gb->build_arg_net(resolved_name, ptr); } // Not found yet — create as net auto [id, net_ptr] = gb->find_or_create_net(resolved_name); net_ptr->as_Net()->destinations().push_back(node_entry); - return {id, net_ptr}; + return gb->build_arg_net(id, net_ptr); }; if (is_expr) { @@ -606,7 +605,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // $N remap int remap_idx = i - bang_offset; while ((int)nb.remaps.size() <= remap_idx) - nb.remaps.push_back({"$unconnected", gb->find("$unconnected")}); + nb.remaps.push_back(gb->build_arg_net("$unconnected", gb->find("$unconnected"))); nb.remaps[remap_idx] = std::move(arg); } } @@ -666,14 +665,14 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { } // Step 2: Build port_name → ArgNet2 map from inputs array - std::map net_map; + std::map net_map; for (int i = 0; i < (int)cur_inputs.size() && i < (int)old_pin_names.size(); i++) { net_map[old_pin_names[i]] = resolve_net(cur_inputs[i]); } // Step 3: Build port_name → parsed_value map from inlined args // Inlined args cover input_ports[0..num_inline_args-1] - std::map inline_map; + std::map inline_map; if (!args_are_type && nb.parsed_args) { auto info = compute_inline_args(args_joined, old_nt->inputs); int num_inline = std::min(info.num_inline_args, old_nt->inputs); @@ -689,31 +688,30 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { merged->rewrite_input_count = nb.parsed_args->rewrite_input_count; // Helper: find value by port name with fallback for bang→bang_in rename - auto find_by_name = [&](const char* name) -> std::pair { + auto find_by_name = [&](const char* name) -> FlowArg2Ptr { auto net_it = net_map.find(name); if (net_it != net_map.end()) - return {true, std::move(net_it->second)}; + return std::move(net_it->second); auto inline_it = inline_map.find(name); if (inline_it != inline_map.end()) - return {true, std::move(inline_it->second)}; - // Fallback: old "bang" → new "bang_in" + return std::move(inline_it->second); if (strcmp(name, "bang_in") == 0) { auto it2 = net_map.find("bang"); if (it2 != net_map.end()) - return {true, std::move(it2->second)}; + return std::move(it2->second); } - return {false, {}}; + return nullptr; }; // Pass 1: fill by name matching std::vector filled(new_nt->total_inputs(), false); for (int i = 0; i < new_nt->total_inputs(); i++) { - auto [found, value] = find_by_name(new_nt->input_port(i)->name); - if (found) { + auto value = find_by_name(new_nt->input_port(i)->name); + if (value) { merged->push_back(std::move(value)); filled[i] = true; } else { - merged->push_back(resolve_net("")); // placeholder + merged->push_back(resolve_net("")); } } @@ -756,7 +754,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if (nb.parsed_args && nb.parsed_args->rewrite_input_count > (int)nb.remaps.size()) { auto unconnected = gb->find("$unconnected"); while ((int)nb.remaps.size() < nb.parsed_args->rewrite_input_count) - nb.remaps.push_back({"$unconnected", unconnected}); + nb.remaps.push_back(gb->build_arg_net("$unconnected", unconnected)); } // Trim trailing $unconnected optional ports from parsed_args @@ -765,7 +763,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { auto* trim_nt = find_node_type2(nb.type_id); if (trim_nt && nb.parsed_args) { while ((int)nb.parsed_args->size() > trim_nt->num_inputs) { - auto* an = std::get_if(&nb.parsed_args->back()); + auto an = nb.parsed_args->back()->as_net(); if (!an || an->first() != "$unconnected") break; nb.parsed_args->pop_back(); } @@ -819,12 +817,12 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { auto fixup_args = [&](ParsedArgs2* pa) { if (!pa) return; for (auto& a : *pa) { - if (auto* an = std::get_if(&a)) { - if (!an->second() || !an->second()->as_Net()) continue; - auto actual = gb->find(an->first()); - if (actual && actual->as_Node()) - an->entry(actual); - } + auto an = a->as_net(); + if (!an) continue; + if (!an->second() || !an->second()->as_Net()) continue; + auto actual = gb->find(an->first()); + if (actual && actual->as_Node()) + an->entry(actual); } }; for (auto& [id, entry] : gb->entries) { @@ -866,20 +864,19 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if (parent_ptr->parsed_args && shadow_ptr->parsed_args && !shadow_ptr->parsed_args->empty()) { std::string shadow_out_prefix = shadow_id + "-out"; bool replaced = false; - for (auto& a : *parent_ptr->parsed_args) { - if (auto* an = std::get_if(&a)) { - if (an->first().compare(0, shadow_out_prefix.size(), shadow_out_prefix) == 0) { - a = (*shadow_ptr->parsed_args)[0]; - replaced = true; - break; - } + for (int ai = 0; ai < parent_ptr->parsed_args->size(); ai++) { + auto an = (*parent_ptr->parsed_args)[ai]->as_net(); + if (an && an->first().compare(0, shadow_out_prefix.size(), shadow_out_prefix) == 0) { + parent_ptr->parsed_args->set(ai, (*shadow_ptr->parsed_args)[0]); + replaced = true; + break; } } // Fallback: try positional insertion (for nodes without merged inputs) if (!replaced) { while ((int)parent_ptr->parsed_args->size() <= arg_index) - parent_ptr->parsed_args->push_back(std::make_shared(ArgString2{""})); - (*parent_ptr->parsed_args)[arg_index] = (*shadow_ptr->parsed_args)[0]; + parent_ptr->parsed_args->push_back(gb->build_arg_string("")); + parent_ptr->parsed_args->set(arg_index, (*shadow_ptr->parsed_args)[0]); } } @@ -889,12 +886,12 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { auto& sin = sin_it->second; for (int i = 0; i < (int)sin.size(); i++) { while ((int)parent_ptr->remaps.size() <= i) - parent_ptr->remaps.push_back(ArgNet2{"$unconnected", unconnected_entry}); + parent_ptr->remaps.push_back(gb->build_arg_net("$unconnected", unconnected_entry)); if (!sin[i].empty()) { auto net_ptr = gb->find(sin[i]); if (net_ptr) { - parent_ptr->remaps[i] = ArgNet2{sin[i], net_ptr}; + parent_ptr->remaps[i] = gb->build_arg_net(sin[i], net_ptr); if (auto net = net_ptr->as_Net()) { auto& dests = net->destinations(); @@ -983,9 +980,9 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { }; // Helper: rename ArgNet2 in-place - auto remap_arg = [&](FlowArg2& a) { - if (auto* an = std::get_if(&a)) - an->id(remap_id(an->first())); + auto remap_arg = [&](const FlowArg2Ptr& a) { + if (auto n = a->as_net()) + n->net_id(remap_id(n->first())); }; auto remap_args = [&](ParsedArgs2* pa) { if (!pa) return; @@ -998,8 +995,8 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if (!node_p) continue; remap_args(node_p->parsed_args.get()); remap_args(node_p->parsed_va_args.get()); - for (auto& r : node_p->remaps) r.id(remap_id(r.first())); - for (auto& o : node_p->outputs) o.id(remap_id(o.first())); + for (auto& r : node_p->remaps) if (auto n = r->as_net()) n->net_id(remap_id(n->first())); + for (auto& o : node_p->outputs) if (auto n = o->as_net()) n->net_id(remap_id(n->first())); } // Rebuild entries map with new keys From 55ea34c6d4a2ba8b9af566be6743e451dc41a723 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 17:19:32 +0200 Subject: [PATCH 51/86] $empty, $unconnected --- src/atto/graph_builder.cpp | 50 ++++++++++++++++++++++++++------------ src/atto/graph_builder.h | 8 +++++- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index c012771..53574c3 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -264,16 +264,35 @@ std::shared_ptr GraphBuilder::add_node(NodeId id, NodeTypeID ty return nb; } -void GraphBuilder::ensure_unconnected() { - if (entries.count("$unconnected")) return; - auto net = std::make_shared(shared_from_this()); - net->is_the_unconnected(true); - net->auto_wire(true); - net->id("$unconnected"); - entries["$unconnected"] = net; +FlowNodeBuilderPtr GraphBuilder::empty_node() { + ensure_sentinels(); + return empty_; +} + +NetBuilderPtr GraphBuilder::unconnected_net() { + ensure_sentinels(); + return unconnected_; +} + +void GraphBuilder::ensure_sentinels() { + if (!unconnected_) { + unconnected_ = std::make_shared(shared_from_this()); + unconnected_->is_the_unconnected(true); + unconnected_->auto_wire(true); + unconnected_->id("$unconnected"); + entries["$unconnected"] = unconnected_; + } + if (!empty_) { + empty_ = std::make_shared(shared_from_this()); + empty_->is_the_empty = true; + empty_->id("$empty"); + entries["$empty"] = empty_; + } } std::pair GraphBuilder::find_or_create_net(const NodeId& name, bool for_source) { + if (name == "$unconnected" || name == "$empty") + throw std::logic_error("find_or_create_net: use unconnected_net()/empty_node() for sentinel '" + name + "'"); auto it = entries.find(name); if (it != entries.end()) { if (auto net = it->second->as_Net()) { @@ -291,6 +310,8 @@ std::pair GraphBuilder::find_or_create_net(const NodeId } BuilderEntryPtr GraphBuilder::find(const NodeId& id) { + if (id == "$unconnected" || id == "$empty") + throw std::logic_error("find: use unconnected_net()/empty_node() for sentinel '" + id + "'"); auto it = entries.find(id); return (it != entries.end()) ? it->second : nullptr; } @@ -437,7 +458,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { } auto gb = std::make_shared(); - gb->ensure_unconnected(); + gb->ensure_sentinels(); bool in_node = false; std::string cur_id, cur_type; @@ -505,7 +526,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // (it's implicit from NodeKind2::Flow) if (!is_post_bang) { while ((int)nb.outputs.size() <= i) - nb.outputs.push_back(gb->build_arg_net("$unconnected", gb->find("$unconnected"))); + nb.outputs.push_back(gb->build_arg_net("$unconnected", gb->unconnected_net())); nb.outputs[i] = std::move(arg); } } @@ -539,7 +560,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // Map to new descriptor order if (new_nt) { nb.outputs.resize(new_nt->num_outputs); - auto unconnected = gb->find("$unconnected"); + auto unconnected = gb->unconnected_net(); for (int i = 0; i < new_nt->num_outputs; i++) { const char* name = new_nt->output_ports[i].name; auto it = out_net_map.find(name); @@ -569,8 +590,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // Helper: resolve net/node name to ArgNet2 and register destination auto resolve_net = [&](const std::string& net_name) -> FlowArg2Ptr { if (net_name.empty()) { - auto [resolved, ptr] = gb->find_or_create_net("$unconnected"); - return gb->build_arg_net(resolved, ptr); + return gb->build_arg_net("$unconnected", gb->unconnected_net()); } // Strip -as_lambda suffix → resolve to node entry directly std::string resolved_name = net_name; @@ -605,7 +625,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // $N remap int remap_idx = i - bang_offset; while ((int)nb.remaps.size() <= remap_idx) - nb.remaps.push_back(gb->build_arg_net("$unconnected", gb->find("$unconnected"))); + nb.remaps.push_back(gb->build_arg_net("$unconnected", gb->unconnected_net())); nb.remaps[remap_idx] = std::move(arg); } } @@ -752,7 +772,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // Ensure remaps are sized to rewrite_input_count (from $N refs in expressions) if (nb.parsed_args && nb.parsed_args->rewrite_input_count > (int)nb.remaps.size()) { - auto unconnected = gb->find("$unconnected"); + auto unconnected = gb->unconnected_net(); while ((int)nb.remaps.size() < nb.parsed_args->rewrite_input_count) nb.remaps.push_back(gb->build_arg_net("$unconnected", unconnected)); } @@ -834,7 +854,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { } // ─── Fold shadow nodes into parents ─── - auto unconnected_entry = gb->find("$unconnected"); + auto unconnected_entry = gb->unconnected_net(); // Collect shadow ids std::vector shadow_ids; diff --git a/src/atto/graph_builder.h b/src/atto/graph_builder.h index 56141a0..c3d9639 100644 --- a/src/atto/graph_builder.h +++ b/src/atto/graph_builder.h @@ -259,6 +259,7 @@ struct FlowNodeBuilder: BuilderEntry { Outputs outputs; Vec2 position = {0, 0}; bool shadow = false; + bool is_the_empty = false; // true for the special $empty sentinel node std::string error; std::string args_str() const; @@ -276,7 +277,10 @@ struct GraphBuilder : std::enable_shared_from_this { std::shared_ptr add_node(NodeId id, NodeTypeID type, std::shared_ptr args); - void ensure_unconnected(); + // Sentinel accessors (created once, cached) + FlowNodeBuilderPtr empty_node(); // the $empty sentinel node + NetBuilderPtr unconnected_net(); // the $unconnected sentinel net + void ensure_sentinels(); // create both if not yet created std::pair find_or_create_net(const NodeId& name, bool for_source = false); @@ -307,6 +311,8 @@ struct GraphBuilder : std::enable_shared_from_this { private: bool dirty_ = false; std::vector pins_; + FlowNodeBuilderPtr empty_; + NetBuilderPtr unconnected_; }; // ─── Parse/reconstruct helpers ─── From fff59f832a83e6a984139e9611b4bf58eb69b81d Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 17:29:29 +0200 Subject: [PATCH 52/86] $empty/$unconnected always --- src/atto/graph_builder.cpp | 52 +++++++++++++++++++++++++++++++------- src/atto/graph_builder.h | 23 +++++++++-------- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index 53574c3..c47d0e4 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -61,8 +61,41 @@ static std::vector parse_toml_array(const std::string& val) { static void maybe_dirty(const std::shared_ptr& gb) { if (gb) gb->mark_dirty(); } +FlowArg2::FlowArg2(ArgKind kind, const std::shared_ptr& owner) + : kind_(kind), owner_(owner) + , node_(owner ? owner->empty_node() : nullptr) + , wire_(owner ? owner->unconnected_net() : nullptr) +{ + if (!owner_) throw std::logic_error("FlowArg2: owner must not be null"); + if (!node_) throw std::logic_error("FlowArg2: node must not be null"); + if (!wire_) throw std::logic_error("FlowArg2: wire must not be null"); +} + void FlowArg2::mark_dirty() { maybe_dirty(owner_); } +const FlowNodeBuilderPtr& FlowArg2::node() const { + if (!node_) throw std::logic_error("FlowArg2::node(): node is null"); + return node_; +} +void FlowArg2::node(const FlowNodeBuilderPtr& n) { + if (!n) throw std::logic_error("FlowArg2::node(set): cannot set null, use empty_node()"); + node_ = n; +} + +const NetBuilderPtr& FlowArg2::wire() const { + if (!wire_) throw std::logic_error("FlowArg2::wire(): wire is null"); + return wire_; +} +void FlowArg2::wire(const NetBuilderPtr& w) { + if (!w) throw std::logic_error("FlowArg2::wire(set): cannot set null, use unconnected_net()"); + wire_ = w; +} + +const std::shared_ptr& FlowArg2::owner() const { + if (!owner_) throw std::logic_error("FlowArg2::owner(): owner is null"); + return owner_; +} + std::shared_ptr FlowArg2::as_net() { return kind_ == ArgKind::Net ? std::dynamic_pointer_cast(shared_from_this()) : nullptr; } @@ -77,15 +110,10 @@ std::shared_ptr FlowArg2::as_expr() { } std::string FlowArg2::name() const { - // Build: "node_name.port_name" or "node_name.va_name[idx]" - std::string prefix; - auto n = node_.lock(); - if (n) prefix = n->id(); - else prefix = "$empty"; + std::string prefix = node_->id(); if (port_) { - // TODO: Check if this is a va_arg by looking at the node's va_args - // For now, just use port name + // TODO: for va_args, append [idx] return prefix + "." + port_->name; } return prefix + ".?"; @@ -93,8 +121,14 @@ std::string FlowArg2::name() const { // ─── Dirty-tracked setters ─── -void ArgNet2::net_id(const NodeId& v) { net_id_ = v; mark_dirty(); } -void ArgNet2::entry(std::shared_ptr v) { entry_ = std::move(v); mark_dirty(); } +void ArgNet2::net_id(const NodeId& v) { + if (v.empty()) throw std::logic_error("ArgNet2::net_id: cannot set empty id"); + net_id_ = v; mark_dirty(); +} +void ArgNet2::entry(std::shared_ptr v) { + if (!v) throw std::logic_error("ArgNet2::entry: cannot set null entry"); + entry_ = std::move(v); mark_dirty(); +} void ArgNumber2::value(double v) { value_ = v; mark_dirty(); } void ArgNumber2::is_float(bool v) { is_float_ = v; mark_dirty(); } void ArgString2::value(const std::string& v) { value_ = v; mark_dirty(); } diff --git a/src/atto/graph_builder.h b/src/atto/graph_builder.h index c3d9639..211d1b9 100644 --- a/src/atto/graph_builder.h +++ b/src/atto/graph_builder.h @@ -49,32 +49,31 @@ struct FlowArg2 : std::enable_shared_from_this { std::shared_ptr as_string(); std::shared_ptr as_expr(); - // Context: which node/wire/port this arg belongs to - std::shared_ptr node() const { return node_.lock(); } - void node(const std::shared_ptr& n) { node_ = n; } + // Context: which node/wire/port this arg belongs to (always valid, never null) + const FlowNodeBuilderPtr& node() const; + void node(const FlowNodeBuilderPtr& n); - std::shared_ptr wire() const { return wire_.lock(); } - void wire(const std::shared_ptr& w) { wire_ = w; } + const NetBuilderPtr& wire() const; + void wire(const NetBuilderPtr& w); const PortDesc2* port() const { return port_; } void port(const PortDesc2* p) { port_ = p; } - std::shared_ptr owner() const { return owner_; } + const std::shared_ptr& owner() const; // Computed name: "node.port_name" or "node.va_name[idx]" etc. std::string name() const; protected: - FlowArg2(ArgKind kind, const std::shared_ptr& owner) - : kind_(kind), owner_(owner) {} + FlowArg2(ArgKind kind, const std::shared_ptr& owner); void mark_dirty(); private: ArgKind kind_; std::shared_ptr owner_; - std::weak_ptr node_; - std::weak_ptr wire_; + FlowNodeBuilderPtr node_; // always valid ($empty if unassigned) + NetBuilderPtr wire_; // always valid ($unconnected if unassigned) const PortDesc2* port_ = nullptr; }; @@ -98,7 +97,9 @@ struct ArgNet2 : FlowArg2 { private: ArgNet2(NodeId id, std::shared_ptr entry, const std::shared_ptr& owner) - : FlowArg2(ArgKind::Net, owner), net_id_(std::move(id)), entry_(std::move(entry)) {} + : FlowArg2(ArgKind::Net, owner), net_id_(std::move(id)), entry_(std::move(entry)) { + if (!entry_) throw std::logic_error("ArgNet2: entry must not be null"); + } NodeId net_id_; std::shared_ptr entry_; From 5d9897272861b1ab4c4ef5f260d06841fc36228b Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 17:31:48 +0200 Subject: [PATCH 53/86] refactor: consolidate FlowNodeBuilder and NetBuilder pointer type definitions --- src/atto/graph_builder.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/atto/graph_builder.h b/src/atto/graph_builder.h index 211d1b9..8e37cee 100644 --- a/src/atto/graph_builder.h +++ b/src/atto/graph_builder.h @@ -26,6 +26,9 @@ struct GraphBuilder; struct FlowNodeBuilder; struct NetBuilder; +using FlowNodeBuilderPtr = std::shared_ptr; +using NetBuilderPtr = std::shared_ptr; + // ─── FlowArg2: base class for all pin/arg types ─── struct PortDesc2; // forward From 3e9d3b1ecf076be9c9edd06dbc299e3db0045a94 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 17:32:42 +0200 Subject: [PATCH 54/86] alias fix from last commit --- src/atto/graph_builder.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/atto/graph_builder.h b/src/atto/graph_builder.h index 8e37cee..8952abc 100644 --- a/src/atto/graph_builder.h +++ b/src/atto/graph_builder.h @@ -246,8 +246,6 @@ struct NetBuilder: BuilderEntry { std::vector destinations_; }; -using NetBuilderPtr = std::shared_ptr; - // ─── FlowNodeBuilder ─── using Remaps = std::vector; @@ -269,8 +267,6 @@ struct FlowNodeBuilder: BuilderEntry { std::string args_str() const; }; -using FlowNodeBuilderPtr = std::shared_ptr; - using BuilderResult = std::variant, BuilderError>; // ─── GraphBuilder ─── From 1c6868732e6fed03e5198629535247d69736b928 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 17:40:39 +0200 Subject: [PATCH 55/86] feat: add find_or_null_node method to GraphBuilder and update usages --- src/atto/graph_builder.cpp | 7 +++- src/atto/graph_builder.h | 2 ++ src/attoflow/editor2.cpp | 65 ++++++++++++++++++-------------------- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index c47d0e4..c49b720 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -343,6 +343,11 @@ std::pair GraphBuilder::find_or_create_net(const NodeId return {entries.find(name)->first, net}; } +BuilderEntryPtr GraphBuilder::find_or_null_node(const NodeId& id) { + auto it = entries.find(id); + return (it != entries.end()) ? it->second : nullptr; +} + BuilderEntryPtr GraphBuilder::find(const NodeId& id) { if (id == "$unconnected" || id == "$empty") throw std::logic_error("find: use unconnected_net()/empty_node() for sentinel '" + id + "'"); @@ -874,7 +879,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { auto an = a->as_net(); if (!an) continue; if (!an->second() || !an->second()->as_Net()) continue; - auto actual = gb->find(an->first()); + auto actual = gb->find_or_null_node(an->first()); if (actual && actual->as_Node()) an->entry(actual); } diff --git a/src/atto/graph_builder.h b/src/atto/graph_builder.h index 8952abc..bfa1acb 100644 --- a/src/atto/graph_builder.h +++ b/src/atto/graph_builder.h @@ -284,6 +284,8 @@ struct GraphBuilder : std::enable_shared_from_this { std::pair find_or_create_net(const NodeId& name, bool for_source = false); + + BuilderEntryPtr find_or_null_node(const NodeId& id); BuilderEntryPtr find(const NodeId& id); FlowNodeBuilderPtr find_node(const NodeId& id); diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 6833c82..be9718d 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -111,7 +111,7 @@ struct PinMapping { // Base args: track which parsed_args indices are ArgNet2 if (node->parsed_args) { for (int i = 0; i < parsed_size; i++) { - if (std::holds_alternative((*node->parsed_args)[i])) { + if ((*node->parsed_args)[i]->is(ArgKind::Net)) { m.pin_to_port.push_back(i); m.base_count++; } @@ -129,7 +129,7 @@ struct PinMapping { // Va_args if (node->parsed_va_args) { for (int i = 0; i < (int)node->parsed_va_args->size(); i++) { - if (std::holds_alternative((*node->parsed_va_args)[i])) { + if ((*node->parsed_va_args)[i]->is(ArgKind::Net)) { m.pin_to_port.push_back(-(i + 1)); // negative = va_args index (1-based) m.va_count++; } @@ -315,7 +315,8 @@ void Editor2Pane::draw() { if (!src_node) return; named = !net->auto_wire(); for (int k = 0; k < (int)src_node->outputs.size(); k++) { - if (src_node->outputs[k].second() == entry) { + auto out_net = src_node->outputs[k]->as_net(); + if (out_net && out_net->second() == entry) { source_pin = k; break; } @@ -386,19 +387,21 @@ void Editor2Pane::draw() { if (dst_pm.is_base(i)) { int port = dst_pm.port_index(i); if (dst_node->parsed_args && port < (int)dst_node->parsed_args->size()) { - if (auto* an = std::get_if(&(*dst_node->parsed_args)[port])) + if (auto an = (*dst_node->parsed_args)[port]->as_net()) draw_wire_to_pin(i, an->second(), an->first()); } } else if (dst_pm.is_va(i)) { int va_idx = -(dst_pm.port_index(i) + 1); if (dst_node->parsed_va_args && va_idx < (int)dst_node->parsed_va_args->size()) { - if (auto* an = std::get_if(&(*dst_node->parsed_va_args)[va_idx])) + if (auto an = (*dst_node->parsed_va_args)[va_idx]->as_net()) draw_wire_to_pin(i, an->second(), an->first()); } } else if (dst_pm.is_remap(i)) { int ri = dst_pm.remap_index(i); - if (ri < (int)dst_node->remaps.size()) - draw_wire_to_pin(i, dst_node->remaps[ri].second(), dst_node->remaps[ri].first()); + if (ri < (int)dst_node->remaps.size()) { + if (auto an = dst_node->remaps[ri]->as_net()) + draw_wire_to_pin(i, an->second(), an->first()); + } } } } @@ -581,9 +584,9 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, // Display first arg without quotes std::string display; if (node->parsed_args && !node->parsed_args->empty()) { - auto& a = (*node->parsed_args)[0]; - if (auto* s = std::get_if(&a)) display = s->value(); - else if (auto* e = std::get_if(&a)) display = e->expr(); + auto a = (*node->parsed_args)[0]; + if (auto s = a->as_string()) display = s->value(); + else if (auto e = a->as_expr()) display = e->expr(); else display = node->args_str(); } @@ -856,38 +859,30 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, ImGui::BeginTooltip(); ImGui::SetWindowFontScale(S.tooltip_scale); ImGui::Text("id: %s", node->id().c_str()); - if (node->parsed_args) { - ImGui::Text("parsed_args (%d):", (int)node->parsed_args->size()); - for (int i = 0; i < (int)node->parsed_args->size(); i++) { - auto& a = (*node->parsed_args)[i]; - if (auto* n = std::get_if(&a)) + auto show_args = [](const char* label, const ParsedArgs2* pa) { + if (!pa) return; + ImGui::Text("%s (%d):", label, pa->size()); + for (int i = 0; i < pa->size(); i++) { + auto a = (*pa)[i]; + if (auto n = a->as_net()) ImGui::Text(" [%d] net: %s", i, n->first().c_str()); - else if (auto* e = std::get_if(&a)) + else if (auto e = a->as_expr()) ImGui::Text(" [%d] expr: %s", i, e->expr().c_str()); - else if (auto* s = std::get_if(&a)) + else if (auto s = a->as_string()) ImGui::Text(" [%d] str: %s", i, s->value().c_str()); - else if (auto* v = std::get_if(&a)) + else if (auto v = a->as_number()) ImGui::Text(" [%d] num: %g", i, v->value()); } - } - if (node->parsed_va_args && !node->parsed_va_args->empty()) { - ImGui::Text("parsed_va_args (%d):", (int)node->parsed_va_args->size()); - for (int i = 0; i < (int)node->parsed_va_args->size(); i++) { - auto& a = (*node->parsed_va_args)[i]; - if (auto* n = std::get_if(&a)) - ImGui::Text(" [%d] net: %s", i, n->first().c_str()); - else if (auto* e = std::get_if(&a)) - ImGui::Text(" [%d] expr: %s", i, e->expr().c_str()); - else if (auto* s = std::get_if(&a)) - ImGui::Text(" [%d] str: %s", i, s->value().c_str()); - else if (auto* v = std::get_if(&a)) - ImGui::Text(" [%d] num: %g", i, v->value()); - } - } + }; + show_args("parsed_args", node->parsed_args.get()); + if (node->parsed_va_args && !node->parsed_va_args->empty()) + show_args("parsed_va_args", node->parsed_va_args.get()); if (!node->remaps.empty()) { ImGui::Text("remaps (%d):", (int)node->remaps.size()); - for (int i = 0; i < (int)node->remaps.size(); i++) - ImGui::Text(" $%d -> %s", i, node->remaps[i].first().c_str()); + for (int i = 0; i < (int)node->remaps.size(); i++) { + if (auto n = node->remaps[i]->as_net()) + ImGui::Text(" $%d -> %s", i, n->first().c_str()); + } } ImGui::EndTooltip(); } From d6ce2bd411266f34fd840b06fd4356580d3ae7a0 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 17:50:03 +0200 Subject: [PATCH 56/86] add remap and pin index methods to FlowArg2 and update GraphBuilder arg factory signatures --- src/atto/graph_builder.cpp | 76 +++++++++++++++++++++++++++++++------- src/atto/graph_builder.h | 12 ++++-- 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index c49b720..6ee4452 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -119,6 +119,41 @@ std::string FlowArg2::name() const { return prefix + ".?"; } +unsigned FlowArg2::remap_idx() const { + if (!is_remap()) throw std::logic_error("FlowArg2::remap_idx(): not a remap (port is set)"); + auto n = node(); + auto self = const_cast(this)->shared_from_this(); + for (unsigned i = 0; i < n->remaps.size(); i++) { + if (n->remaps[i] == self) return i; + } + throw std::logic_error("FlowArg2::remap_idx(): arg not found in node remaps"); +} + +unsigned FlowArg2::input_pin_idx() const { + if (is_remap()) throw std::logic_error("FlowArg2::input_pin_idx(): is a remap (no port)"); + auto n = node(); + auto self = const_cast(this)->shared_from_this(); + if (n->parsed_args) { + for (unsigned i = 0; i < (unsigned)n->parsed_args->size(); i++) + if ((*n->parsed_args)[i] == self) return i; + } + if (n->parsed_va_args) { + unsigned base = n->parsed_args ? (unsigned)n->parsed_args->size() : 0; + for (unsigned i = 0; i < (unsigned)n->parsed_va_args->size(); i++) + if ((*n->parsed_va_args)[i] == self) return base + i; + } + throw std::logic_error("FlowArg2::input_pin_idx(): arg not found in node inputs"); +} + +unsigned FlowArg2::output_pin_idx() const { + if (is_remap()) throw std::logic_error("FlowArg2::output_pin_idx(): is a remap (no port)"); + auto n = node(); + auto self = const_cast(this)->shared_from_this(); + for (unsigned i = 0; i < (unsigned)n->outputs.size(); i++) + if (n->outputs[i] == self) return i; + throw std::logic_error("FlowArg2::output_pin_idx(): arg not found in node outputs"); +} + // ─── Dirty-tracked setters ─── void ArgNet2::net_id(const NodeId& v) { @@ -399,23 +434,27 @@ bool GraphBuilder::rename(const BuilderEntryPtr& entry, const NodeId& new_id) { return true; } -FlowArg2Ptr GraphBuilder::build_arg_net(NodeId id, BuilderEntryPtr entry) { +FlowArg2Ptr GraphBuilder::build_arg_net(NodeId id, BuilderEntryPtr entry, const PortDesc2* port) { auto p = std::shared_ptr(new ArgNet2{std::move(id), std::move(entry), shared_from_this()}); + if (port) p->port(port); pins_.push_back(p); return p; } -FlowArg2Ptr GraphBuilder::build_arg_number(double value, bool is_float) { +FlowArg2Ptr GraphBuilder::build_arg_number(double value, bool is_float, const PortDesc2* port) { auto p = std::shared_ptr(new ArgNumber2{value, is_float, shared_from_this()}); + if (port) p->port(port); pins_.push_back(p); return p; } -FlowArg2Ptr GraphBuilder::build_arg_string(std::string value) { +FlowArg2Ptr GraphBuilder::build_arg_string(std::string value, const PortDesc2* port) { auto p = std::shared_ptr(new ArgString2{std::move(value), shared_from_this()}); + if (port) p->port(port); pins_.push_back(p); return p; } -FlowArg2Ptr GraphBuilder::build_arg_expr(std::string expr) { +FlowArg2Ptr GraphBuilder::build_arg_expr(std::string expr, const PortDesc2* port) { auto p = std::shared_ptr(new ArgExpr2{std::move(expr), shared_from_this()}); + if (port) p->port(port); pins_.push_back(p); return p; } @@ -601,18 +640,21 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { nb.outputs.resize(new_nt->num_outputs); auto unconnected = gb->unconnected_net(); for (int i = 0; i < new_nt->num_outputs; i++) { - const char* name = new_nt->output_ports[i].name; - auto it = out_net_map.find(name); + auto* pd = &new_nt->output_ports[i]; + auto it = out_net_map.find(pd->name); if (it != out_net_map.end()) { + it->second->port(pd); nb.outputs[i] = std::move(it->second); - } else if (strcmp(name, "next") == 0) { + } else if (strcmp(pd->name, "next") == 0) { auto it2 = out_net_map.find("bang"); - if (it2 != out_net_map.end()) + if (it2 != out_net_map.end()) { + it2->second->port(pd); nb.outputs[i] = std::move(it2->second); - else - nb.outputs[i] = gb->build_arg_net("$unconnected", unconnected); + } else { + nb.outputs[i] = gb->build_arg_net("$unconnected", unconnected, pd); + } } else { - nb.outputs[i] = gb->build_arg_net("$unconnected", unconnected); + nb.outputs[i] = gb->build_arg_net("$unconnected", unconnected, pd); } } } @@ -765,12 +807,16 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // Pass 1: fill by name matching std::vector filled(new_nt->total_inputs(), false); for (int i = 0; i < new_nt->total_inputs(); i++) { - auto value = find_by_name(new_nt->input_port(i)->name); + auto* pd = new_nt->input_port(i); + auto value = find_by_name(pd->name); if (value) { + value->port(pd); merged->push_back(std::move(value)); filled[i] = true; } else { - merged->push_back(resolve_net("")); + auto placeholder = resolve_net(""); + placeholder->port(pd); + merged->push_back(std::move(placeholder)); } } @@ -787,7 +833,9 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { for (int i = 0; i < new_nt->total_inputs(); i++) { if (!filled[i] && new_nt->input_port(i)->kind != PortKind2::BangTrigger) { if (arg_cursor < (int)nb.parsed_args->size()) { - merged->set(i, std::move((*nb.parsed_args)[arg_cursor++])); + auto arg = std::move((*nb.parsed_args)[arg_cursor++]); + arg->port(new_nt->input_port(i)); + merged->set(i, std::move(arg)); filled[i] = true; } } diff --git a/src/atto/graph_builder.h b/src/atto/graph_builder.h index bfa1acb..1616326 100644 --- a/src/atto/graph_builder.h +++ b/src/atto/graph_builder.h @@ -61,6 +61,10 @@ struct FlowArg2 : std::enable_shared_from_this { const PortDesc2* port() const { return port_; } void port(const PortDesc2* p) { port_ = p; } + bool is_remap() const { return port_ == nullptr; } + unsigned remap_idx() const; // throws if !is_remap() + unsigned input_pin_idx() const; // throws if is_remap(); looks in parsed_args + parsed_va_args + unsigned output_pin_idx() const; // throws if is_remap(); looks in outputs const std::shared_ptr& owner() const; @@ -299,10 +303,10 @@ struct GraphBuilder : std::enable_shared_from_this { bool rename(const BuilderEntryPtr& entry, const NodeId& new_id); // Arg factories — all pins are tracked in pins_ - FlowArg2Ptr build_arg_net(NodeId id, BuilderEntryPtr entry); - FlowArg2Ptr build_arg_number(double value, bool is_float); - FlowArg2Ptr build_arg_string(std::string value); - FlowArg2Ptr build_arg_expr(std::string expr); + FlowArg2Ptr build_arg_net(NodeId id, BuilderEntryPtr entry, const PortDesc2* port = nullptr); + FlowArg2Ptr build_arg_number(double value, bool is_float, const PortDesc2* port = nullptr); + FlowArg2Ptr build_arg_string(std::string value, const PortDesc2* port = nullptr); + FlowArg2Ptr build_arg_expr(std::string expr, const PortDesc2* port = nullptr); const std::vector& pins() const { return pins_; } From a0d3c334b7f4f739837924d2243fa840646dab76 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 17:59:26 +0200 Subject: [PATCH 57/86] FlowArg2::name --- src/atto/graph_builder.cpp | 13 +++++++++---- src/atto/node_types2.h | 39 ++++++++++++++++++++++---------------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index 6ee4452..3c7e788 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -112,11 +112,16 @@ std::shared_ptr FlowArg2::as_expr() { std::string FlowArg2::name() const { std::string prefix = node_->id(); - if (port_) { - // TODO: for va_args, append [idx] - return prefix + "." + port_->name; + if (!is_remap()) { + prefix = prefix + "." + port_->name; + if (port_->va_args) { + return prefix + "[" + std::to_string(input_pin_idx()) + "]"; + } else { + return prefix; + } + } else { + return prefix + ".remaps[" + std::to_string(remap_idx()) + "]"; } - return prefix + ".?"; } unsigned FlowArg2::remap_idx() const { diff --git a/src/atto/node_types2.h b/src/atto/node_types2.h index 29b2186..cac6a5f 100644 --- a/src/atto/node_types2.h +++ b/src/atto/node_types2.h @@ -10,12 +10,19 @@ enum class PortKind2 : uint8_t { BangNext, // bang output (rendered as square) }; +enum class PortPosition2: uint8_t { + Input, + Output, +}; + struct PortDesc2 { const char* name; const char* desc; PortKind2 kind = PortKind2::Data; + PortPosition2 position = PortPosition2::Input; const char* type_name = nullptr; bool optional = false; + bool va_args = false; }; enum class NodeKind2 : uint8_t { @@ -57,14 +64,14 @@ struct NodeType2 { // Common outputs static const PortDesc2 P2_NEXT[] = { - {.name = "next", .desc = "fires after completion", .kind = PortKind2::BangNext}, + {.name = "next", .desc = "fires after completion", .kind = PortKind2::BangNext, .position = PortPosition2::Output}, }; static const PortDesc2 P2_RESULT[] = { - {.name = "result", .desc = "result value"}, + {.name = "result", .desc = "result value", .position = PortPosition2::Output}, }; static const PortDesc2 P2_NEXT_RESULT[] = { - {.name = "next", .desc = "fires after completion", .kind = PortKind2::BangNext}, - {.name = "result", .desc = "result value"}, + {.name = "next", .desc = "fires after completion", .kind = PortKind2::BangNext, .position = PortPosition2::Output}, + {.name = "result", .desc = "result value", .position = PortPosition2::Output}, }; // Common inputs @@ -124,15 +131,15 @@ static const PortDesc2 P2_SELECT_BANG_IN[] = { {.name = "condition", .desc = "boolean condition"}, }; static const PortDesc2 P2_SELECT_BANG_OUT[] = { - {.name = "next", .desc = "fires after branch completes", .kind = PortKind2::BangNext}, - {.name = "true", .desc = "fires when true", .kind = PortKind2::BangNext}, - {.name = "false", .desc = "fires when false", .kind = PortKind2::BangNext}, + {.name = "next", .desc = "fires after branch completes", .kind = PortKind2::BangNext, .position = PortPosition2::Output}, + {.name = "true", .desc = "fires when true", .kind = PortKind2::BangNext, .position = PortPosition2::Output}, + {.name = "false", .desc = "fires when false", .kind = PortKind2::BangNext, .position = PortPosition2::Output}, }; // va_args templates -static const PortDesc2 P2_VA_FIELD = {.name = "field", .desc = "constructor field"}; -static const PortDesc2 P2_VA_ARG = {.name = "arg", .desc = "function argument"}; -static const PortDesc2 P2_VA_PARAM = {.name = "param", .desc = "lambda parameter"}; +static const PortDesc2 P2_VA_FIELD = {.name = "field", .desc = "constructor field", .va_args = true}; +static const PortDesc2 P2_VA_ARG = {.name = "arg", .desc = "function argument", .va_args = true}; +static const PortDesc2 P2_VA_PARAM = {.name = "param", .desc = "lambda parameter", .va_args = true}; // new static const PortDesc2 P2_NEW_IN[] = { @@ -177,8 +184,8 @@ static const PortDesc2 P2_DECL_TYPE_IN[] = { {.name = "type", .desc = "type definition"}, }; static const PortDesc2 P2_DECL_TYPE_OUT[] = { - {.name = "next", .desc = "fires after declaration", .kind = PortKind2::BangNext}, - {.name = "type", .desc = "the declared type"}, + {.name = "next", .desc = "fires after declaration", .kind = PortKind2::BangNext, .position = PortPosition2::Output}, + {.name = "type", .desc = "the declared type", .position = PortPosition2::Output}, }; static const PortDesc2 P2_DECL_VAR_IN[] = { {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, @@ -189,11 +196,11 @@ static const PortDesc2 P2_DECL_VAR_OPT_IN[] = { {.name = "initial", .desc = "variable initial value", .optional = true}, }; static const PortDesc2 P2_DECL_VAR_OUT[] = { - {.name = "next", .desc = "fires after declaration", .kind = PortKind2::BangNext}, - {.name = "ref", .desc = "reference to variable"}, + {.name = "next", .desc = "fires after declaration", .kind = PortKind2::BangNext, .position = PortPosition2::Output}, + {.name = "ref", .desc = "reference to variable", .position = PortPosition2::Output}, }; static const PortDesc2 P2_DECL_OUT[] = { - {.name = "next", .desc = "fires to start declarations", .kind = PortKind2::BangNext}, + {.name = "next", .desc = "fires to start declarations", .kind = PortKind2::BangNext, .position = PortPosition2::Output}, }; static const PortDesc2 P2_DECL_EVENT_IN[] = { {.name = "bang_in", .desc = "trigger", .kind = PortKind2::BangTrigger}, @@ -231,7 +238,7 @@ static const PortDesc2 P2_RESIZE_IN[] = { // event! static const PortDesc2 P2_EVENT_OUT[] = { - {.name = "next", .desc = "fires on event", .kind = PortKind2::BangNext}, + {.name = "next", .desc = "fires on event", .kind = PortKind2::BangNext, .position = PortPosition2::Output}, }; // ─── Node type table ─── From d55cf3671b6cca53da0b418dab52de62bc3c6b09 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 18:05:46 +0200 Subject: [PATCH 58/86] wire -> net in grahp_builder --- src/atto/graph_builder.cpp | 20 ++++++++++---------- src/atto/graph_builder.h | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index 3c7e788..2e132b2 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -64,11 +64,11 @@ static void maybe_dirty(const std::shared_ptr& gb) { if (gb) gb->m FlowArg2::FlowArg2(ArgKind kind, const std::shared_ptr& owner) : kind_(kind), owner_(owner) , node_(owner ? owner->empty_node() : nullptr) - , wire_(owner ? owner->unconnected_net() : nullptr) + , net_(owner ? owner->unconnected_net() : nullptr) { if (!owner_) throw std::logic_error("FlowArg2: owner must not be null"); if (!node_) throw std::logic_error("FlowArg2: node must not be null"); - if (!wire_) throw std::logic_error("FlowArg2: wire must not be null"); + if (!net_) throw std::logic_error("FlowArg2: net must not be null"); } void FlowArg2::mark_dirty() { maybe_dirty(owner_); } @@ -82,13 +82,13 @@ void FlowArg2::node(const FlowNodeBuilderPtr& n) { node_ = n; } -const NetBuilderPtr& FlowArg2::wire() const { - if (!wire_) throw std::logic_error("FlowArg2::wire(): wire is null"); - return wire_; +const NetBuilderPtr& FlowArg2::net() const { + if (!net_) throw std::logic_error("FlowArg2::net(): net is null"); + return net_; } -void FlowArg2::wire(const NetBuilderPtr& w) { - if (!w) throw std::logic_error("FlowArg2::wire(set): cannot set null, use unconnected_net()"); - wire_ = w; +void FlowArg2::net(const NetBuilderPtr& w) { + if (!w) throw std::logic_error("FlowArg2::net(set): cannot set null, use unconnected_net()"); + net_ = w; } const std::shared_ptr& FlowArg2::owner() const { @@ -583,7 +583,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { bool is_expr = is_any_of(nb.type_id, NodeTypeID::Expr, NodeTypeID::ExprBang); int old_num_nexts = old_nt ? old_nt->num_nexts : 0; - // Helper: wire a net and return ArgNet2 + // Helper: net a net and return ArgNet2 auto wire_output = [&](const std::string& net_name) -> FlowArg2Ptr { auto [resolved, net_ptr] = gb->find_or_create_net(net_name, true); if (auto net = net_ptr ? net_ptr->as_Net() : nullptr) @@ -591,7 +591,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { return gb->build_arg_net(resolved, net_ptr); }; - // Filter out empty and -as_lambda entries, wire all nets + // Filter out empty and -as_lambda entries, net all nets // For expr: outputs are all data (no nexts), dynamic count // For others: [nexts..., data_outs..., post_bang] if (is_expr) { diff --git a/src/atto/graph_builder.h b/src/atto/graph_builder.h index 1616326..c75c914 100644 --- a/src/atto/graph_builder.h +++ b/src/atto/graph_builder.h @@ -52,12 +52,12 @@ struct FlowArg2 : std::enable_shared_from_this { std::shared_ptr as_string(); std::shared_ptr as_expr(); - // Context: which node/wire/port this arg belongs to (always valid, never null) + // Context: which node/net/port this arg belongs to (always valid, never null) const FlowNodeBuilderPtr& node() const; void node(const FlowNodeBuilderPtr& n); - const NetBuilderPtr& wire() const; - void wire(const NetBuilderPtr& w); + const NetBuilderPtr& net() const; + void net(const NetBuilderPtr& w); const PortDesc2* port() const { return port_; } void port(const PortDesc2* p) { port_ = p; } @@ -80,7 +80,7 @@ struct FlowArg2 : std::enable_shared_from_this { ArgKind kind_; std::shared_ptr owner_; FlowNodeBuilderPtr node_; // always valid ($empty if unassigned) - NetBuilderPtr wire_; // always valid ($unconnected if unassigned) + NetBuilderPtr net_; // always valid ($unconnected if unassigned) const PortDesc2* port_ = nullptr; }; From 5444e94d69f1892152a13f6b9a98a6d1223923d3 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 18:11:37 +0200 Subject: [PATCH 59/86] Wire has entry now --- src/attoflow/editor2.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index be9718d..fa9c3a8 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -84,10 +84,13 @@ static float point_to_bezier_dist(ImVec2 p, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImV // Stored wire info for hover hit-testing struct WireInfo { - ImVec2 p0, p1, p2, p3; // bezier control points - NodeId src_id, dst_id; // source and destination node IDs - NodeId net_id; // net name - bool is_lambda = false; + BuilderEntryPtr entry; // the net entry (null for lambda wires) + + ImVec2 p0, p1, p2, p3; // bezier control points + NodeId src_id, dst_id; // source and destination node IDs + NodeId net_id; // net name + + bool is_lambda() const { return entry->is(IdCategory::Node); }; }; static inline ImVec2 v2add(ImVec2 a, ImVec2 b) { return {a.x + b.x, a.y + b.y}; } @@ -360,7 +363,7 @@ void Editor2Pane::draw() { } dl->AddBezierCubic(from, cp1, cp2, to, wire_col, th); - drawn_wires.push_back({from, cp1, cp2, to, src_node->id(), dst_id, net_id, is_lambda}); + drawn_wires.push_back({ entry, from, cp1, cp2, to, src_node->id(), dst_id, net_id}); // Label for named nets if (named) { @@ -448,7 +451,7 @@ void Editor2Pane::draw() { best_wire_info->p2, best_wire_info->p3, S.col_pin_hover, th); ImGui::BeginTooltip(); ImGui::SetWindowFontScale(S.tooltip_scale); - if (best_wire_info->is_lambda) + if (best_wire_info->is_lambda()) ImGui::Text("lambda: %s", best_wire_info->src_id.c_str()); else ImGui::Text("net: %s", best_wire_info->net_id.c_str()); From cd8b1d67db0184a47f0a9a8e8bc74656cb1effdd Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 18:13:34 +0200 Subject: [PATCH 60/86] cosmetics --- src/attoflow/editor2.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index fa9c3a8..fff724c 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -84,13 +84,14 @@ static float point_to_bezier_dist(ImVec2 p, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImV // Stored wire info for hover hit-testing struct WireInfo { - BuilderEntryPtr entry; // the net entry (null for lambda wires) + BuilderEntryPtr entry_; // the net entry (null for lambda wires) ImVec2 p0, p1, p2, p3; // bezier control points NodeId src_id, dst_id; // source and destination node IDs NodeId net_id; // net name - bool is_lambda() const { return entry->is(IdCategory::Node); }; + BuilderEntryPtr entry() { return entry_; } + bool is_lambda() const { return entry_->is(IdCategory::Node); }; }; static inline ImVec2 v2add(ImVec2 a, ImVec2 b) { return {a.x + b.x, a.y + b.y}; } From 9771f1e5f4389e0e703cf1ffd73cbb43243ff857 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 18:20:16 +0200 Subject: [PATCH 61/86] wire hover functionality to highlight all wires sharing the same entry and source node for lambdas --- src/attoflow/editor2.cpp | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index fff724c..d803ee8 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -91,6 +91,7 @@ struct WireInfo { NodeId net_id; // net name BuilderEntryPtr entry() { return entry_; } + const BuilderEntryPtr entry() const { return entry_; } bool is_lambda() const { return entry_->is(IdCategory::Node); }; }; @@ -433,23 +434,39 @@ void Editor2Pane::draw() { if (hover_item_.expired()) { float wire_thresh = S.wire_hit_threshold * canvas_zoom_; float best_dist = wire_thresh; - BuilderEntryPtr best_wire; const WireInfo* best_wire_info = nullptr; for (auto& w : drawn_wires) { float d = point_to_bezier_dist(mouse, w.p0, w.p1, w.p2, w.p3); if (d < best_dist) { best_dist = d; - auto entry = gb_->find(w.net_id); - if (entry) { best_wire = entry; best_wire_info = &w; } + if (w.entry()) best_wire_info = &w; } } - if (best_wire) hover_item_ = best_wire; - // Draw wire highlight + tooltip if (best_wire_info) { + hover_item_ = best_wire_info->entry(); + auto hover_entry = best_wire_info->entry(); float th = (S.wire_thickness + 2.0f) * canvas_zoom_; - dl->AddBezierCubic(best_wire_info->p0, best_wire_info->p1, - best_wire_info->p2, best_wire_info->p3, S.col_pin_hover, th); + + // Highlight ALL wires sharing the same entry (same net or same lambda node) + for (auto& w : drawn_wires) { + if (w.entry() == hover_entry) { + dl->AddBezierCubic(w.p0, w.p1, w.p2, w.p3, S.col_pin_hover, th); + } + } + + // For lambdas, also highlight the source node + if (best_wire_info->is_lambda()) { + auto src = hover_entry->as_Node(); + if (src) { + auto src_layout = compute_node_layout(src, canvas_origin, canvas_zoom_); + dl->AddRect(src_layout.pos, + {src_layout.pos.x + src_layout.width, src_layout.pos.y + src_layout.height}, + S.col_pin_hover, S.node_rounding * canvas_zoom_, 0, S.highlight_thickness); + } + } + + // Tooltip ImGui::BeginTooltip(); ImGui::SetWindowFontScale(S.tooltip_scale); if (best_wire_info->is_lambda()) From 06177b0aea791ef1a721521de43e74ef0de85772 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 18:47:52 +0200 Subject: [PATCH 62/86] mostly unified highlights --- src/atto/graph_builder.cpp | 15 ++ src/attoflow/editor2.cpp | 467 ++++++++++++++++++++++--------------- src/attoflow/editor2.h | 27 ++- 3 files changed, 315 insertions(+), 194 deletions(-) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index 2e132b2..7e71ca3 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -1119,6 +1119,21 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { gb->entries = std::move(new_entries); } + // ─── Assign node() on all pins ─── + for (auto& [id, entry] : gb->entries) { + auto node_p = entry->as_Node(); + if (!node_p) continue; + auto assign_node = [&](ParsedArgs2* pa) { + if (!pa) return; + for (int i = 0; i < pa->size(); i++) + (*pa)[i]->node(node_p); + }; + assign_node(node_p->parsed_args.get()); + assign_node(node_p->parsed_va_args.get()); + for (auto& r : node_p->remaps) r->node(node_p); + for (auto& o : node_p->outputs) o->node(node_p); + } + gb->compact(); return gb; diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index d803ee8..1f6e096 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -82,18 +82,7 @@ static float point_to_bezier_dist(ImVec2 p, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImV return std::sqrt(min_d2); } -// Stored wire info for hover hit-testing -struct WireInfo { - BuilderEntryPtr entry_; // the net entry (null for lambda wires) - - ImVec2 p0, p1, p2, p3; // bezier control points - NodeId src_id, dst_id; // source and destination node IDs - NodeId net_id; // net name - - BuilderEntryPtr entry() { return entry_; } - const BuilderEntryPtr entry() const { return entry_; } - bool is_lambda() const { return entry_->is(IdCategory::Node); }; -}; +// WireInfo defined in editor2.h static inline ImVec2 v2add(ImVec2 a, ImVec2 b) { return {a.x + b.x, a.y + b.y}; } static inline ImVec2 v2sub(ImVec2 a, ImVec2 b) { return {a.x - b.x, a.y - b.y}; } @@ -413,75 +402,22 @@ void Editor2Pane::draw() { dl->PopClipRect(); - // ─── Determine hover item (node or wire) ─── - hover_item_.reset(); + // ─── Hover detection + effects ─── if (canvas_hovered) { ImVec2 mouse = ImGui::GetIO().MousePos; - - // Check nodes first (they have priority over wires) - for (auto it = gb_->entries.rbegin(); it != gb_->entries.rend(); ++it) { - auto node = it->second->as_Node(); - if (!node) continue; - auto layout = compute_node_layout(node, canvas_origin, canvas_zoom_); - if (mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && - mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height) { - hover_item_ = node; - break; - } - } - - // If no node hit, check wires - if (hover_item_.expired()) { - float wire_thresh = S.wire_hit_threshold * canvas_zoom_; - float best_dist = wire_thresh; - const WireInfo* best_wire_info = nullptr; - for (auto& w : drawn_wires) { - float d = point_to_bezier_dist(mouse, w.p0, w.p1, w.p2, w.p3); - if (d < best_dist) { - best_dist = d; - if (w.entry()) best_wire_info = &w; - } - } - - if (best_wire_info) { - hover_item_ = best_wire_info->entry(); - auto hover_entry = best_wire_info->entry(); - float th = (S.wire_thickness + 2.0f) * canvas_zoom_; - - // Highlight ALL wires sharing the same entry (same net or same lambda node) - for (auto& w : drawn_wires) { - if (w.entry() == hover_entry) { - dl->AddBezierCubic(w.p0, w.p1, w.p2, w.p3, S.col_pin_hover, th); - } - } - - // For lambdas, also highlight the source node - if (best_wire_info->is_lambda()) { - auto src = hover_entry->as_Node(); - if (src) { - auto src_layout = compute_node_layout(src, canvas_origin, canvas_zoom_); - dl->AddRect(src_layout.pos, - {src_layout.pos.x + src_layout.width, src_layout.pos.y + src_layout.height}, - S.col_pin_hover, S.node_rounding * canvas_zoom_, 0, S.highlight_thickness); - } - } - - // Tooltip - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(S.tooltip_scale); - if (best_wire_info->is_lambda()) - ImGui::Text("lambda: %s", best_wire_info->src_id.c_str()); - else - ImGui::Text("net: %s", best_wire_info->net_id.c_str()); - ImGui::Text("src: %s", best_wire_info->src_id.c_str()); - ImGui::Text("dst: %s", best_wire_info->dst_id.c_str()); - ImGui::EndTooltip(); - } - } + hover_item_ = detect_hover(mouse, canvas_origin, drawn_wires); + } else { + hover_item_ = std::monostate{}; + } + draw_hover_effects(dl, canvas_origin, drawn_wires, hover_item_); + + // Extract hover node from variant + FlowNodeBuilderPtr hover_node = nullptr; + if (auto* ep = std::get_if(&hover_item_)) { + if (*ep) hover_node = (*ep)->as_Node(); + } else if (auto* pin = std::get_if(&hover_item_)) { + hover_node = (*pin)->node(); } - - auto hover_locked = hover_item_.lock(); - auto hover_node = hover_locked ? hover_locked->as_Node() : nullptr; // ─── Selection + dragging with left mouse ─── if (canvas_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { @@ -639,15 +575,23 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, if (!args.empty()) display += " " + args; bool selected = selected_nodes_.count(node); - bool hovered = (hover_item_.lock() == node); + // node_hovered = hover_item_ is this node directly (not a pin on it) + bool node_hovered = false; + if (auto* ep = std::get_if(&hover_item_)) + node_hovered = (*ep == node); + + // pin_hovered_on_this_node = hover_item_ is a pin belonging to this node + bool pin_hovered_on_this = false; + if (auto* pin = std::get_if(&hover_item_)) + pin_hovered_on_this = ((*pin)->node() == node); bool has_error = !node->error.empty(); ImU32 col = has_error ? S.col_node_err : (selected ? S.col_node_sel : S.col_node); dl->AddRectFilled(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, col, S.node_rounding * canvas_zoom_); dl->AddRect(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, - hovered ? S.col_pin_hover : S.col_node_border, S.node_rounding * canvas_zoom_, - 0, hovered ? S.highlight_thickness : 1.0f); + node_hovered ? S.col_pin_hover : S.col_node_border, S.node_rounding * canvas_zoom_, + 0, node_hovered ? S.highlight_thickness : 1.0f); // Text float font_size = ImGui::GetFontSize() * canvas_zoom_; @@ -741,142 +685,121 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, {gp.x - pr, gp.y}, {gp.x + pr, gp.y + pr}, lc); + // Outline when node is hovered (not pin) + if (node_hovered) { + float ho = S.highlight_offset * canvas_zoom_; + dl->AddTriangle( + {gp.x + pr + ho, gp.y - pr - ho}, + {gp.x - pr - ho, gp.y}, + {gp.x + pr + ho, gp.y + pr + ho}, + S.col_pin_hover, S.highlight_thickness); + } // Side-bang (square, middle-right) ImVec2 bp = {layout.pos.x + layout.width, layout.pos.y + layout.height * 0.5f}; dl->AddRectFilled({bp.x - pr, bp.y - pr}, {bp.x + pr, bp.y + pr}, S.col_pin_bang); } - // Pin hover: highlight + tooltip - ImVec2 mouse = ImGui::GetMousePos(); - float hit_r = pr * S.pin_hit_radius_mul; - auto dist2 = [](ImVec2 a, ImVec2 b) { return (a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y); }; - float hit_r2 = hit_r * hit_r; + // Pin/node hover visuals driven by hover_item_ + if (!node_hovered && !pin_hovered_on_this) return; + float ho = S.highlight_offset * canvas_zoom_; ImU32 COL_HOVER = S.col_pin_hover; + float ht = S.highlight_thickness; - // Helper to get input pin name using PinMapping - auto get_input_pin_name = [&](int i) -> const char* { - if (pm.is_absent_optional(i)) { - int port = pm.absent_port_index(i); - if (auto* pd = nt->input_port(port)) return pd->name; - } else if (pm.is_base(i)) { - int port = pm.port_index(i); - if (auto* pd = nt->input_port(port)) return pd->name; - } else if (pm.is_va(i)) { - return nt->va_args ? nt->va_args->name : "va"; - } else if (pm.is_remap(i)) { - static char remap_buf[16]; - snprintf(remap_buf, sizeof(remap_buf), "$%d", pm.remap_index(i)); - return remap_buf; - } - return "?"; - }; - - // Helper to get input pin shape for highlight enum class PinShape2 { Circle, Square, Diamond, TriangleDown, TriangleLeft }; - auto get_input_pin_shape = [&](int i) -> PinShape2 { - if (pm.is_add_diamond(i)) return PinShape2::Diamond; - if (pm.is_absent_optional(i)) return PinShape2::Diamond; - if (pm.is_base(i)) { - int port = pm.port_index(i); - if (auto* pd = nt->input_port(port)) { - if (pd->kind == PortKind2::BangTrigger) return PinShape2::Square; - if (pd->kind == PortKind2::Lambda) return PinShape2::TriangleDown; - if (pd->optional) return PinShape2::Diamond; - } - } else if (pm.is_va(i)) { - return PinShape2::Diamond; - } - return PinShape2::Circle; - }; - - float ht = S.highlight_thickness; auto draw_highlight = [&](ImVec2 pos, PinShape2 shape) { switch (shape) { - case PinShape2::Circle: - dl->AddCircle(pos, pr + ho, COL_HOVER, 0, ht); - break; - case PinShape2::Square: - dl->AddRect({pos.x - pr - ho, pos.y - pr - ho}, {pos.x + pr + ho, pos.y + pr + ho}, COL_HOVER, 0, 0, ht); - break; - case PinShape2::Diamond: - dl->AddQuad({pos.x, pos.y - pr - ho}, {pos.x + pr + ho, pos.y}, {pos.x, pos.y + pr + ho}, {pos.x - pr - ho, pos.y}, COL_HOVER, ht); - break; - case PinShape2::TriangleDown: - dl->AddTriangle({pos.x - pr - ho, pos.y - pr - ho}, {pos.x + pr + ho, pos.y - pr - ho}, {pos.x, pos.y + pr + ho}, COL_HOVER, ht); - break; - case PinShape2::TriangleLeft: - dl->AddTriangle({pos.x + pr + ho, pos.y - pr - ho}, {pos.x - pr - ho, pos.y}, {pos.x + pr + ho, pos.y + pr + ho}, COL_HOVER, ht); - break; + case PinShape2::Circle: dl->AddCircle(pos, pr + ho, COL_HOVER, 0, ht); break; + case PinShape2::Square: dl->AddRect({pos.x-pr-ho,pos.y-pr-ho},{pos.x+pr+ho,pos.y+pr+ho}, COL_HOVER, 0, 0, ht); break; + case PinShape2::Diamond: dl->AddQuad({pos.x,pos.y-pr-ho},{pos.x+pr+ho,pos.y},{pos.x,pos.y+pr+ho},{pos.x-pr-ho,pos.y}, COL_HOVER, ht); break; + case PinShape2::TriangleDown: dl->AddTriangle({pos.x-pr-ho,pos.y-pr-ho},{pos.x+pr+ho,pos.y-pr-ho},{pos.x,pos.y+pr+ho}, COL_HOVER, ht); break; + case PinShape2::TriangleLeft: dl->AddTriangle({pos.x+pr+ho,pos.y-pr-ho},{pos.x-pr-ho,pos.y},{pos.x+pr+ho,pos.y+pr+ho}, COL_HOVER, ht); break; } }; - bool pin_hovered = false; - // Check input pins - for (int i = 0; i < layout.num_in; i++) { - ImVec2 pp = layout.input_pin_pos(i); - if (dist2(mouse, pp) < hit_r2) { - draw_highlight(pp, pm.is_add_diamond(i) ? PinShape2::Diamond : get_input_pin_shape(i)); - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(S.tooltip_scale); - if (pm.is_add_diamond(i)) { - const char* va_name = nt->va_args ? nt->va_args->name : "arg"; - ImGui::Text("add %s", va_name); - } else { - ImGui::Text("%s", get_input_pin_name(i)); + // Get the hovered pin (if any) + FlowArg2Ptr hovered_pin = nullptr; + if (auto* pp = std::get_if(&hover_item_)) + hovered_pin = *pp; + + if (hovered_pin) { + // Find which visual pin matches and highlight it + // Input pins + for (int i = 0; i < pm.total(); i++) { + if (pm.is_add_diamond(i) || pm.is_absent_optional(i)) continue; + FlowArg2Ptr pin_arg = nullptr; + if (pm.is_base(i)) { + int port = pm.port_index(i); + if (node->parsed_args && port < node->parsed_args->size()) + pin_arg = (*node->parsed_args)[port]; + } else if (pm.is_va(i)) { + int vi = -(pm.port_index(i) + 1); + if (node->parsed_va_args && vi < node->parsed_va_args->size()) + pin_arg = (*node->parsed_va_args)[vi]; + } else if (pm.is_remap(i)) { + int ri = pm.remap_index(i); + if (ri < (int)node->remaps.size()) pin_arg = node->remaps[ri]; + } + if (pin_arg == hovered_pin) { + ImVec2 pp = layout.input_pin_pos(i); + auto shape = pm.is_va(i) ? PinShape2::Diamond : PinShape2::Circle; + if (pm.is_base(i)) { + if (auto* pd = nt->input_port(pm.port_index(i))) { + if (pd->kind == PortKind2::BangTrigger) shape = PinShape2::Square; + else if (pd->kind == PortKind2::Lambda) shape = PinShape2::TriangleDown; + else if (pd->optional) shape = PinShape2::Diamond; + } + } + draw_highlight(pp, shape); + if (draw_tooltips_) { + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + if (hovered_pin->port()) + ImGui::Text("%s", hovered_pin->port()->name); + else if (pm.is_remap(i)) { + int ri = pm.remap_index(i); + ImGui::Text("$%d", ri); + } + ImGui::EndTooltip(); + } + return; } - ImGui::EndTooltip(); - pin_hovered = true; - break; } - } - // Check output pins - if (!pin_hovered) { + // Output pins for (int i = 0; i < layout.num_out; i++) { - ImVec2 pp = layout.output_pin_pos(i); - if (dist2(mouse, pp) < hit_r2) { + if (i < (int)node->outputs.size() && node->outputs[i] == hovered_pin) { + ImVec2 pp = layout.output_pin_pos(i); PortKind2 kind = (nt->output_ports && i < nt->num_outputs) ? nt->output_ports[i].kind : PortKind2::Data; draw_highlight(pp, kind == PortKind2::BangNext ? PinShape2::Square : PinShape2::Circle); - const char* name = (nt->output_ports && i < nt->num_outputs) ? nt->output_ports[i].name : "out"; + if (draw_tooltips_) { + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + if (hovered_pin->port()) + ImGui::Text("%s", hovered_pin->port()->name); + else + ImGui::Text("out%d", i); + ImGui::EndTooltip(); + } + return; + } + } + // Side-bang + if (nt->is_flow() && !node->outputs.empty() && node->outputs[0] == hovered_pin) { + ImVec2 bp = {layout.pos.x + layout.width, layout.pos.y + layout.height * 0.5f}; + draw_highlight(bp, PinShape2::Square); + if (draw_tooltips_) { ImGui::BeginTooltip(); ImGui::SetWindowFontScale(S.tooltip_scale); - ImGui::Text("%s", name); + ImGui::Text("post_bang"); ImGui::EndTooltip(); - pin_hovered = true; - break; } - } - } - // Check lambda grab - if (!pin_hovered && nt->is_flow()) { - ImVec2 gp = layout.lambda_grab_pos(); - if (dist2(mouse, gp) < hit_r2) { - draw_highlight(gp, PinShape2::TriangleLeft); - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(S.tooltip_scale); - ImGui::Text("as_lambda"); - ImGui::EndTooltip(); - pin_hovered = true; - } - } - // Check side-bang - if (!pin_hovered && nt->is_flow()) { - ImVec2 bp = {layout.pos.x + layout.width, layout.pos.y + layout.height * 0.5f}; - if (dist2(mouse, bp) < hit_r2) { - draw_highlight(bp, PinShape2::Square); - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(S.tooltip_scale); - ImGui::Text("post_bang"); - ImGui::EndTooltip(); - pin_hovered = true; + return; } } - // Node tooltip (when hovering body, not a pin) - if (!pin_hovered && - mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && - mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height) { + // Node body tooltip (when node is hovered directly, not a pin) + if (node_hovered && draw_tooltips_) { ImGui::BeginTooltip(); ImGui::SetWindowFontScale(S.tooltip_scale); ImGui::Text("id: %s", node->id().c_str()); @@ -912,3 +835,167 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, void Editor2Pane::draw_net(ImDrawList*, const NetBuilderPtr&, ImVec2) { // Unused — wires drawn per-node in draw() } + +// ─── Hover detection ─── + +Editor2Pane::HoverItem Editor2Pane::detect_hover( + ImVec2 mouse, ImVec2 canvas_origin, const std::vector& drawn_wires) +{ + // Priority: pins (most specific) > nodes > wires (least specific) + // But detection order: wires, nodes, pins — last match wins (most specific) + + HoverItem result = std::monostate{}; + + // 1. Wires (lowest priority) + { + float wire_thresh = S.wire_hit_threshold * canvas_zoom_; + float best_dist = wire_thresh; + const WireInfo* best_wire = nullptr; + for (auto& w : drawn_wires) { + float d = point_to_bezier_dist(mouse, w.p0, w.p1, w.p2, w.p3); + if (d < best_dist) { + best_dist = d; + if (w.entry()) best_wire = &w; + } + } + if (best_wire) + result = best_wire->entry(); + } + + // 2. Nodes (overrides wire if hit) + FlowNodeBuilderPtr hit_node = nullptr; + for (auto it = gb_->entries.rbegin(); it != gb_->entries.rend(); ++it) { + auto node = it->second->as_Node(); + if (!node) continue; + auto layout = compute_node_layout(node, canvas_origin, canvas_zoom_); + if (mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && + mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height) { + hit_node = node; + result = BuilderEntryPtr(node); + break; + } + } + + // 3. Pins on hit node (overrides node if hit) + if (hit_node) { + auto* nt = find_node_type2(hit_node->type_id); + if (nt) { + auto layout = compute_node_layout(hit_node, canvas_origin, canvas_zoom_); + float pr = S.pin_radius * canvas_zoom_; + float hit_r = pr * S.pin_hit_radius_mul; + float hit_r2 = hit_r * hit_r; + auto d2 = [](ImVec2 a, ImVec2 b) { return (a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y); }; + auto pm = PinMapping::build(hit_node, nt); + + // Input pins + for (int i = 0; i < pm.total(); i++) { + if (pm.is_add_diamond(i) || pm.is_absent_optional(i)) continue; + ImVec2 pp = layout.input_pin_pos(i); + if (d2(mouse, pp) < hit_r2) { + if (pm.is_base(i)) { + int port = pm.port_index(i); + if (hit_node->parsed_args && port < hit_node->parsed_args->size()) + result = (*hit_node->parsed_args)[port]; + } else if (pm.is_va(i)) { + int vi = -(pm.port_index(i) + 1); + if (hit_node->parsed_va_args && vi < hit_node->parsed_va_args->size()) + result = (*hit_node->parsed_va_args)[vi]; + } else if (pm.is_remap(i)) { + int ri = pm.remap_index(i); + if (ri < (int)hit_node->remaps.size()) + result = hit_node->remaps[ri]; + } + return result; + } + } + + // Output pins + for (int i = 0; i < layout.num_out; i++) { + ImVec2 pp = layout.output_pin_pos(i); + if (d2(mouse, pp) < hit_r2) { + if (i < (int)hit_node->outputs.size() && hit_node->outputs[i]) + result = hit_node->outputs[i]; + return result; + } + } + + // Lambda grab → node itself + if (nt->is_flow() && d2(mouse, layout.lambda_grab_pos()) < hit_r2) + return BuilderEntryPtr(hit_node); + + // Side-bang → first output + if (nt->is_flow()) { + ImVec2 bp = {layout.pos.x + layout.width, layout.pos.y + layout.height * 0.5f}; + if (d2(mouse, bp) < hit_r2) { + if (hit_node->outputs.empty() || !hit_node->outputs[0]) + throw std::logic_error("Flow node '" + hit_node->id() + "' missing side-bang output[0]"); + return hit_node->outputs[0]; + } + } + } + } + + return result; +} + +// ─── Hover effects + tooltips ─── + +void Editor2Pane::draw_hover_effects( + ImDrawList* dl, ImVec2 canvas_origin, + const std::vector& drawn_wires, const HoverItem& hover) +{ + if (std::holds_alternative(hover)) return; + + float th_wire = (S.wire_thickness + 2.0f) * canvas_zoom_; + + // Determine what's hovered + FlowNodeBuilderPtr hover_node = nullptr; + BuilderEntryPtr hover_entry = nullptr; + FlowArg2Ptr hover_pin = nullptr; + + if (auto* ep = std::get_if(&hover)) { + hover_entry = *ep; + hover_node = hover_entry ? hover_entry->as_Node() : nullptr; + } else if (auto* pp = std::get_if(&hover)) { + hover_pin = *pp; + } + + // Node hovered: highlight lambda wires capturing it + if (hover_node) { + for (auto& w : drawn_wires) { + if (w.is_lambda() && w.entry() == hover_node) + dl->AddBezierCubic(w.p0, w.p1, w.p2, w.p3, S.col_pin_hover, th_wire); + } + } + + // Wire/net hovered: highlight all wires in the same net + lambda source node + if (hover_entry && hover_entry->as_Net()) { + for (auto& w : drawn_wires) { + if (w.entry() == hover_entry) + dl->AddBezierCubic(w.p0, w.p1, w.p2, w.p3, S.col_pin_hover, th_wire); + } + if (draw_tooltips_) { + // Find the first wire for tooltip info + for (auto& w : drawn_wires) { + if (w.entry() == hover_entry) { + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + if (w.is_lambda()) + ImGui::Text("lambda: %s", w.src_id.c_str()); + else + ImGui::Text("net: %s", w.net_id.c_str()); + ImGui::Text("src: %s", w.src_id.c_str()); + ImGui::Text("dst: %s", w.dst_id.c_str()); + ImGui::EndTooltip(); + break; + } + } + } + } + + // Lambda node hovered via wire: highlight the source node + if (hover_entry && hover_entry->as_Node() && !hover_node) { + // This case doesn't happen — if entry is a node, hover_node is set. + // Lambda source highlighting is handled above. + } +} diff --git a/src/attoflow/editor2.h b/src/attoflow/editor2.h index 9a3a17f..c24fc1c 100644 --- a/src/attoflow/editor2.h +++ b/src/attoflow/editor2.h @@ -31,12 +31,31 @@ class Editor2Pane { float canvas_zoom_ = 1.0f; // Interaction state - BuilderEntryWeak hover_item_; // current hovered item (node or net), empty = nothing - // TODO: pins will be part of hover_item_ system (needs Pin as a graph entity or lightweight ID) + using HoverItem = std::variant; + HoverItem hover_item_; // monostate = nothing, BuilderEntryPtr = node/net, FlowArg2Ptr = pin + bool draw_tooltips_ = true; std::set selected_nodes_; bool dragging_started_ = false; - bool drag_was_overlapping_ = false; // true if node was overlapping when drag began - int editing_link_id_ = -1; // not used yet, placeholder + bool drag_was_overlapping_ = false; + int editing_link_id_ = -1; + + // Wire info for hover hit-testing + struct WireInfo { + BuilderEntryPtr entry_; + ImVec2 p0, p1, p2, p3; + NodeId src_id, dst_id, net_id; + BuilderEntryPtr entry() const { return entry_; } + bool is_lambda() const { return entry_ && entry_->is(IdCategory::Node); } + }; + + // Hover detection — returns best hover match + HoverItem detect_hover(ImVec2 mouse, ImVec2 canvas_origin, + const std::vector& drawn_wires); + + // Tooltip + highlight drawing (driven by hover_item parameter) + void draw_hover_effects(ImDrawList* dl, ImVec2 canvas_origin, + const std::vector& drawn_wires, + const HoverItem& hover); // Drawing helpers void draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, From c86850c5b481baf276326341c0abd6346605db15 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 18:52:53 +0200 Subject: [PATCH 63/86] refine hover detection logic for pins, nodes, and wires; adjust thresholds and priorities --- src/attoflow/editor2.cpp | 167 +++++++++++++++++++++------------------ 1 file changed, 92 insertions(+), 75 deletions(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 1f6e096..689bdd4 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -29,8 +29,10 @@ static struct { // Hit testing float pin_hit_radius_mul = 2.5f; - float wire_hit_threshold = 60.0f; - float dismiss_radius = 20.0f; // click within this of empty space/wire dismisses selection + float wire_hit_threshold = 30.0f; + float node_hit_threshold_mul = 6.f; // multiplied by pin_radius * zoom + float dismiss_radius = 20.0f; + float pin_priority_bias = 1e6f; // pins always win over nodes/wires when within threshold // Canvas colors ImU32 col_bg = IM_COL32(30, 30, 40, 255); @@ -841,96 +843,111 @@ void Editor2Pane::draw_net(ImDrawList*, const NetBuilderPtr&, ImVec2) { Editor2Pane::HoverItem Editor2Pane::detect_hover( ImVec2 mouse, ImVec2 canvas_origin, const std::vector& drawn_wires) { - // Priority: pins (most specific) > nodes > wires (least specific) - // But detection order: wires, nodes, pins — last match wins (most specific) + // Smallest distance wins across wires, nodes, and pins + auto d2 = [](ImVec2 a, ImVec2 b) { return std::sqrt((a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y)); }; + float best_dist = 1e18f; HoverItem result = std::monostate{}; - // 1. Wires (lowest priority) - { - float wire_thresh = S.wire_hit_threshold * canvas_zoom_; - float best_dist = wire_thresh; - const WireInfo* best_wire = nullptr; - for (auto& w : drawn_wires) { - float d = point_to_bezier_dist(mouse, w.p0, w.p1, w.p2, w.p3); - if (d < best_dist) { - best_dist = d; - if (w.entry()) best_wire = &w; - } + // Pins get a large bias so they always win over nodes/wires when within threshold + float pin_bias = S.pin_priority_bias; + + auto try_candidate = [&](float dist, HoverItem candidate) { + if (dist < best_dist) { + best_dist = dist; + result = std::move(candidate); } - if (best_wire) - result = best_wire->entry(); + }; + + // Wires + float wire_thresh = S.wire_hit_threshold * canvas_zoom_; + for (auto& w : drawn_wires) { + float d = point_to_bezier_dist(mouse, w.p0, w.p1, w.p2, w.p3); + if (d < wire_thresh) + try_candidate(d, w.entry()); } - // 2. Nodes (overrides wire if hit) - FlowNodeBuilderPtr hit_node = nullptr; + // Nodes — distance from mouse to nearest edge of node rect for (auto it = gb_->entries.rbegin(); it != gb_->entries.rend(); ++it) { auto node = it->second->as_Node(); if (!node) continue; auto layout = compute_node_layout(node, canvas_origin, canvas_zoom_); - if (mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && - mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height) { - hit_node = node; - result = BuilderEntryPtr(node); - break; + + // Distance from mouse to nearest point on node outline + float nd; + bool inside = mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && + mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height; + if (inside) { + // Inside: distance to nearest edge + float dl_ = mouse.x - layout.pos.x; + float dr = layout.pos.x + layout.width - mouse.x; + float dt = mouse.y - layout.pos.y; + float db = layout.pos.y + layout.height - mouse.y; + nd = std::min({dl_, dr, dt, db}); + } else { + float cx = std::clamp(mouse.x, layout.pos.x, layout.pos.x + layout.width); + float cy = std::clamp(mouse.y, layout.pos.y, layout.pos.y + layout.height); + nd = d2(mouse, {cx, cy}); } + if (nd < S.pin_radius * canvas_zoom_ * S.node_hit_threshold_mul) + try_candidate(nd, BuilderEntryPtr(node)); } - // 3. Pins on hit node (overrides node if hit) - if (hit_node) { - auto* nt = find_node_type2(hit_node->type_id); - if (nt) { - auto layout = compute_node_layout(hit_node, canvas_origin, canvas_zoom_); - float pr = S.pin_radius * canvas_zoom_; - float hit_r = pr * S.pin_hit_radius_mul; - float hit_r2 = hit_r * hit_r; - auto d2 = [](ImVec2 a, ImVec2 b) { return (a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y); }; - auto pm = PinMapping::build(hit_node, nt); - - // Input pins - for (int i = 0; i < pm.total(); i++) { - if (pm.is_add_diamond(i) || pm.is_absent_optional(i)) continue; - ImVec2 pp = layout.input_pin_pos(i); - if (d2(mouse, pp) < hit_r2) { - if (pm.is_base(i)) { - int port = pm.port_index(i); - if (hit_node->parsed_args && port < hit_node->parsed_args->size()) - result = (*hit_node->parsed_args)[port]; - } else if (pm.is_va(i)) { - int vi = -(pm.port_index(i) + 1); - if (hit_node->parsed_va_args && vi < hit_node->parsed_va_args->size()) - result = (*hit_node->parsed_va_args)[vi]; - } else if (pm.is_remap(i)) { - int ri = pm.remap_index(i); - if (ri < (int)hit_node->remaps.size()) - result = hit_node->remaps[ri]; - } - return result; - } - } + // Pins — check all nodes, find closest pin globally + for (auto it = gb_->entries.rbegin(); it != gb_->entries.rend(); ++it) { + auto node = it->second->as_Node(); + if (!node) continue; + auto* nt = find_node_type2(node->type_id); + if (!nt) continue; + auto layout = compute_node_layout(node, canvas_origin, canvas_zoom_); + float pin_thresh = S.pin_radius * canvas_zoom_ * S.pin_hit_radius_mul; + auto pm = PinMapping::build(node, nt); - // Output pins - for (int i = 0; i < layout.num_out; i++) { - ImVec2 pp = layout.output_pin_pos(i); - if (d2(mouse, pp) < hit_r2) { - if (i < (int)hit_node->outputs.size() && hit_node->outputs[i]) - result = hit_node->outputs[i]; - return result; + // Input pins + for (int i = 0; i < pm.total(); i++) { + if (pm.is_add_diamond(i) || pm.is_absent_optional(i)) continue; + float pd = d2(mouse, layout.input_pin_pos(i)); + if (pd < pin_thresh) { + FlowArg2Ptr pin_arg = nullptr; + if (pm.is_base(i)) { + int port = pm.port_index(i); + if (node->parsed_args && port < node->parsed_args->size()) + pin_arg = (*node->parsed_args)[port]; + } else if (pm.is_va(i)) { + int vi = -(pm.port_index(i) + 1); + if (node->parsed_va_args && vi < node->parsed_va_args->size()) + pin_arg = (*node->parsed_va_args)[vi]; + } else if (pm.is_remap(i)) { + int ri = pm.remap_index(i); + if (ri < (int)node->remaps.size()) + pin_arg = node->remaps[ri]; } + if (pin_arg) try_candidate(pd - pin_bias, pin_arg); } + } - // Lambda grab → node itself - if (nt->is_flow() && d2(mouse, layout.lambda_grab_pos()) < hit_r2) - return BuilderEntryPtr(hit_node); - - // Side-bang → first output - if (nt->is_flow()) { - ImVec2 bp = {layout.pos.x + layout.width, layout.pos.y + layout.height * 0.5f}; - if (d2(mouse, bp) < hit_r2) { - if (hit_node->outputs.empty() || !hit_node->outputs[0]) - throw std::logic_error("Flow node '" + hit_node->id() + "' missing side-bang output[0]"); - return hit_node->outputs[0]; - } + // Output pins + for (int i = 0; i < layout.num_out; i++) { + float pd = d2(mouse, layout.output_pin_pos(i)); + if (pd < pin_thresh && i < (int)node->outputs.size() && node->outputs[i]) + try_candidate(pd - pin_bias, node->outputs[i]); + } + + // Lambda grab → node itself (pin-level priority) + if (nt->is_flow()) { + float pd = d2(mouse, layout.lambda_grab_pos()); + if (pd < pin_thresh) + try_candidate(pd - pin_bias, BuilderEntryPtr(node)); + } + + // Side-bang → first output (pin-level priority) + if (nt->is_flow()) { + ImVec2 bp = {layout.pos.x + layout.width, layout.pos.y + layout.height * 0.5f}; + float pd = d2(mouse, bp); + if (pd < pin_thresh) { + if (node->outputs.empty() || !node->outputs[0]) + throw std::logic_error("Flow node '" + node->id() + "' missing side-bang output[0]"); + try_candidate(pd - pin_bias, node->outputs[0]); } } } From ed253194614e7665162d248d32d8f58b85c766b3 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 18:56:42 +0200 Subject: [PATCH 64/86] multi select --- src/attoflow/editor2.cpp | 38 +++++++++++++++++++++++++++++++++++++- src/attoflow/editor2.h | 2 ++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 689bdd4..b8fe3cb 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -459,8 +459,12 @@ void Editor2Pane::draw() { if (drag_was_overlapping_) break; } } else { - // Clicked on empty space or wire — dismiss selection + // Clicked on empty space or wire — start selection rectangle selected_nodes_.clear(); + selection_rect_active_ = true; + ImVec2 mouse = ImGui::GetIO().MousePos; + selection_rect_start_ = {(mouse.x - canvas_origin.x) / canvas_zoom_, + (mouse.y - canvas_origin.y) / canvas_zoom_}; } } @@ -499,8 +503,40 @@ void Editor2Pane::draw() { } } } + + // Selection rectangle — continuously update selection each frame + if (selection_rect_active_) { + ImVec2 mouse = ImGui::GetIO().MousePos; + ImVec2 cur_canvas = {(mouse.x - canvas_origin.x) / canvas_zoom_, + (mouse.y - canvas_origin.y) / canvas_zoom_}; + + float x0 = std::min(selection_rect_start_.x, cur_canvas.x); + float y0 = std::min(selection_rect_start_.y, cur_canvas.y); + float x1 = std::max(selection_rect_start_.x, cur_canvas.x); + float y1 = std::max(selection_rect_start_.y, cur_canvas.y); + + // Draw selection rect + ImVec2 sp0 = {canvas_origin.x + x0 * canvas_zoom_, canvas_origin.y + y0 * canvas_zoom_}; + ImVec2 sp1 = {canvas_origin.x + x1 * canvas_zoom_, canvas_origin.y + y1 * canvas_zoom_}; + dl->AddRectFilled(sp0, sp1, IM_COL32(100, 130, 200, 40)); + dl->AddRect(sp0, sp1, IM_COL32(100, 130, 200, 180), 0, 0, 1.5f); + + // Recalculate selection set every frame + selected_nodes_.clear(); + for (auto& [id, entry] : gb_->entries) { + auto node = entry->as_Node(); + if (!node) continue; + auto layout = compute_node_layout(node, {0,0}, 1.0f); + float nx0 = node->position.x, ny0 = node->position.y; + float nx1 = nx0 + layout.width, ny1 = ny0 + layout.height; + if (nx0 < x1 && nx1 > x0 && ny0 < y1 && ny1 > y0) + selected_nodes_.insert(node); + } + } + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { dragging_started_ = false; + selection_rect_active_ = false; } // Pan with middle mouse or right mouse diff --git a/src/attoflow/editor2.h b/src/attoflow/editor2.h index c24fc1c..699ac2d 100644 --- a/src/attoflow/editor2.h +++ b/src/attoflow/editor2.h @@ -37,6 +37,8 @@ class Editor2Pane { std::set selected_nodes_; bool dragging_started_ = false; bool drag_was_overlapping_ = false; + bool selection_rect_active_ = false; // true when dragging a selection rectangle + ImVec2 selection_rect_start_ = {0, 0}; // canvas-space start point int editing_link_id_ = -1; // Wire info for hover hit-testing From 0915017cdd20b314f40430dcdf74b0a86b9a93fd Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 19:36:59 +0200 Subject: [PATCH 65/86] output_va_args groundwork --- src/atto/graph_builder.cpp | 2 +- src/atto/graph_builder.h | 1 + src/atto/node_types2.h | 80 +++++++++++++++++++++----------------- src/attoflow/editor2.cpp | 20 +++++----- 4 files changed, 56 insertions(+), 47 deletions(-) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index 7e71ca3..00bcbe6 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -1043,7 +1043,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if (!entry->is(IdCategory::Node)) continue; auto& node = *entry->as_Node(); auto* nt = find_node_type2(node.type_id); - if (!nt || !nt->va_args || !node.parsed_args) continue; + if (!nt || !nt->input_ports_va_args || !node.parsed_args) continue; // Split at total descriptor input count (required + optional) int fixed_args = nt->total_inputs(); diff --git a/src/atto/graph_builder.h b/src/atto/graph_builder.h index c75c914..c7ba279 100644 --- a/src/atto/graph_builder.h +++ b/src/atto/graph_builder.h @@ -263,6 +263,7 @@ struct FlowNodeBuilder: BuilderEntry { std::shared_ptr parsed_va_args; Remaps remaps; Outputs outputs; + Outputs outputs_va_args; Vec2 position = {0, 0}; bool shadow = false; bool is_the_empty = false; // true for the special $empty sentinel node diff --git a/src/atto/node_types2.h b/src/atto/node_types2.h index cac6a5f..a760e0a 100644 --- a/src/atto/node_types2.h +++ b/src/atto/node_types2.h @@ -1,7 +1,7 @@ #pragma once #include "node_types.h" // for NodeTypeID -// New pin model: flattened inputs/outputs, optional, va_args +// New pin model: flattened inputs/outputs, optional, input_ports_va_args, output_ports_va_args enum class PortKind2 : uint8_t { BangTrigger, // bang input (rendered as square, top) @@ -34,17 +34,21 @@ enum class NodeKind2 : uint8_t { }; struct NodeType2 { + NodeKind2 kind = NodeKind2::Flow; NodeTypeID type_id; + const char* name; const char* desc; + const PortDesc2* input_ports = nullptr; int num_inputs = 0; // required input ports const PortDesc2* input_optional_ports = nullptr; int num_inputs_optional = 0; // trailing optional input ports + const PortDesc2* input_ports_va_args = nullptr; // nullptr = no input_ports_va_args, else template for repeating pins + const PortDesc2* output_ports; int num_outputs; - NodeKind2 kind = NodeKind2::Flow; - const PortDesc2* va_args = nullptr; // nullptr = no va_args, else template for repeating pins + const PortDesc2* output_ports_va_args = nullptr; // nullptr = no input_ports_va_args, else template for repeating pins int total_inputs() const { return num_inputs + num_inputs_optional; } const PortDesc2* input_port(int i) const { @@ -141,6 +145,11 @@ static const PortDesc2 P2_VA_FIELD = {.name = "field", .desc = "constructor fiel static const PortDesc2 P2_VA_ARG = {.name = "arg", .desc = "function argument", .va_args = true}; static const PortDesc2 P2_VA_PARAM = {.name = "param", .desc = "lambda parameter", .va_args = true}; +// va_args outputs +static const PortDesc2 P2_VA_EVENT_OUT = {.name = "args", .desc = "event arguments", .kind = PortKind2::Data , .position = PortPosition2::Output, .va_args = true}; + +static const PortDesc2 P2_VA_EXPR_OUT = {.name = "expr", .desc = "expression outputs", .kind = PortKind2::Data , .position = PortPosition2::Output, .va_args = true}; + // new static const PortDesc2 P2_NEW_IN[] = { {.name = "type", .desc = "type to instantiate"}, @@ -236,10 +245,6 @@ static const PortDesc2 P2_RESIZE_IN[] = { {.name = "size", .desc = "new size", .type_name = "s32"}, }; -// event! -static const PortDesc2 P2_EVENT_OUT[] = { - {.name = "next", .desc = "fires on event", .kind = PortKind2::BangNext, .position = PortPosition2::Output}, -}; // ─── Node type table ─── @@ -249,7 +254,8 @@ static const NodeType2 NODE_TYPES2[] = { .name = "expr", .desc = "Evaluate expression", .output_ports = P2_RESULT, - .num_outputs = 1 + .num_outputs = 1, + .output_ports_va_args = &P2_VA_EXPR_OUT, }, { .type_id = NodeTypeID::Select, @@ -266,9 +272,9 @@ static const NodeType2 NODE_TYPES2[] = { .desc = "Instantiate a type", .input_ports = P2_NEW_IN, .num_inputs = 1, + .input_ports_va_args = &P2_VA_FIELD, .output_ports = P2_RESULT, .num_outputs = 1, - .va_args = &P2_VA_FIELD }, { .type_id = NodeTypeID::Dup, @@ -296,6 +302,7 @@ static const NodeType2 NODE_TYPES2[] = { .num_outputs = 1 }, { + .kind = NodeKind2::Banged, .type_id = NodeTypeID::DiscardBang, .name = "discard!", .desc = "Discard value, pass bang", @@ -303,7 +310,6 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 2, .output_ports = P2_NEXT, .num_outputs = 1, - .kind = NodeKind2::Banged }, { .type_id = NodeTypeID::Discard, @@ -313,6 +319,7 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 1 }, { + .kind = NodeKind2::Declaration, .type_id = NodeTypeID::DeclType, .name = "decl_type", .desc = "Declare a type", @@ -320,9 +327,9 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 3, .output_ports = P2_DECL_TYPE_OUT, .num_outputs = 2, - .kind = NodeKind2::Declaration }, { + .kind = NodeKind2::Declaration, .type_id = NodeTypeID::DeclVar, .name = "decl_var", .desc = "Declare a variable", @@ -332,17 +339,17 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs_optional = 1, .output_ports = P2_DECL_VAR_OUT, .num_outputs = 2, - .kind = NodeKind2::Declaration }, { + .kind = NodeKind2::Declaration, .type_id = NodeTypeID::Decl, .name = "decl", .desc = "Compile-time entry point", .output_ports = P2_DECL_OUT, .num_outputs = 1, - .kind = NodeKind2::Declaration }, { + .kind = NodeKind2::Declaration, .type_id = NodeTypeID::DeclEvent, .name = "decl_event", .desc = "Declare event", @@ -350,9 +357,9 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 3, .output_ports = P2_NEXT, .num_outputs = 1, - .kind = NodeKind2::Declaration }, { + .kind = NodeKind2::Declaration, .type_id = NodeTypeID::DeclImport, .name = "decl_import", .desc = "Import module", @@ -360,9 +367,9 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 2, .output_ports = P2_NEXT, .num_outputs = 1, - .kind = NodeKind2::Declaration }, { + .kind = NodeKind2::Declaration, .type_id = NodeTypeID::Ffi, .name = "ffi", .desc = "Declare external function", @@ -370,7 +377,6 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 3, .output_ports = P2_NEXT, .num_outputs = 1, - .kind = NodeKind2::Declaration }, { .type_id = NodeTypeID::Call, @@ -378,20 +384,20 @@ static const NodeType2 NODE_TYPES2[] = { .desc = "Call function", .input_ports = P2_CALL_IN, .num_inputs = 1, + .input_ports_va_args = &P2_VA_ARG, .output_ports = P2_RESULT, .num_outputs = 1, - .va_args = &P2_VA_ARG }, { + .kind = NodeKind2::Banged, .type_id = NodeTypeID::CallBang, .name = "call!", .desc = "Call function (bang)", .input_ports = P2_CALL_BANG_IN, .num_inputs = 2, + .input_ports_va_args = &P2_VA_ARG, .output_ports = P2_NEXT_RESULT, .num_outputs = 2, - .kind = NodeKind2::Banged, - .va_args = &P2_VA_ARG }, { .type_id = NodeTypeID::Erase, @@ -403,12 +409,12 @@ static const NodeType2 NODE_TYPES2[] = { .num_outputs = 1 }, { + .kind = NodeKind2::Banged, .type_id = NodeTypeID::OutputMixBang, .name = "output_mix!", .desc = "Mix into audio output", .input_ports = P2_OUTPUT_MIX_IN, .num_inputs = 2, - .kind = NodeKind2::Banged }, { .type_id = NodeTypeID::Append, @@ -420,6 +426,7 @@ static const NodeType2 NODE_TYPES2[] = { .num_outputs = 1 }, { + .kind = NodeKind2::Banged, .type_id = NodeTypeID::AppendBang, .name = "append!", .desc = "Append to collection (bang)", @@ -427,7 +434,6 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 3, .output_ports = P2_NEXT_RESULT, .num_outputs = 2, - .kind = NodeKind2::Banged }, { .type_id = NodeTypeID::Store, @@ -437,6 +443,7 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 2 }, { + .kind = NodeKind2::Banged, .type_id = NodeTypeID::StoreBang, .name = "store!", .desc = "Store value (bang)", @@ -444,29 +451,30 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 3, .output_ports = P2_NEXT, .num_outputs = 1, - .kind = NodeKind2::Banged }, { + .kind = NodeKind2::Event, .type_id = NodeTypeID::EventBang, .name = "event!", .desc = "Event source", - .output_ports = P2_EVENT_OUT, + .output_ports = P2_NEXT, .num_outputs = 1, - .kind = NodeKind2::Event + .output_ports_va_args = &P2_VA_EVENT_OUT, }, { + .kind = NodeKind2::Special, .type_id = NodeTypeID::OnKeyDownBang, .name = "on_key_down!", .desc = "(removed)", - .kind = NodeKind2::Special }, { + .kind = NodeKind2::Special, .type_id = NodeTypeID::OnKeyUpBang, .name = "on_key_up!", .desc = "(removed)", - .kind = NodeKind2::Special }, { + .kind = NodeKind2::Banged, .type_id = NodeTypeID::SelectBang, .name = "select!", .desc = "Branch on condition", @@ -474,9 +482,9 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 2, .output_ports = P2_SELECT_BANG_OUT, .num_outputs = 3, - .kind = NodeKind2::Banged }, { + .kind = NodeKind2::Banged, .type_id = NodeTypeID::ExprBang, .name = "expr!", .desc = "Evaluate expression on bang", @@ -484,9 +492,10 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 1, .output_ports = P2_NEXT, .num_outputs = 1, - .kind = NodeKind2::Banged + .output_ports_va_args = &P2_VA_EXPR_OUT, }, { + .kind = NodeKind2::Banged, .type_id = NodeTypeID::EraseBang, .name = "erase!", .desc = "Erase from collection (bang)", @@ -494,7 +503,6 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 3, .output_ports = P2_NEXT_RESULT, .num_outputs = 2, - .kind = NodeKind2::Banged }, { .type_id = NodeTypeID::Iterate, @@ -504,6 +512,7 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 2 }, { + .kind = NodeKind2::Banged, .type_id = NodeTypeID::IterateBang, .name = "iterate!", .desc = "Iterate collection (bang)", @@ -511,7 +520,6 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 3, .output_ports = P2_NEXT, .num_outputs = 1, - .kind = NodeKind2::Banged }, { .type_id = NodeTypeID::Next, @@ -528,20 +536,21 @@ static const NodeType2 NODE_TYPES2[] = { .desc = "Execute under mutex lock", .input_ports = P2_LOCK_IN, .num_inputs = 2, - .va_args = &P2_VA_PARAM + .input_ports_va_args = &P2_VA_PARAM }, { + .kind = NodeKind2::Banged, .type_id = NodeTypeID::LockBang, .name = "lock!", .desc = "Execute under mutex lock (bang)", .input_ports = P2_LOCK_BANG_IN, .num_inputs = 3, + .input_ports_va_args = &P2_VA_PARAM, .output_ports = P2_NEXT, .num_outputs = 1, - .kind = NodeKind2::Banged, - .va_args = &P2_VA_PARAM }, { + .kind = NodeKind2::Banged, .type_id = NodeTypeID::ResizeBang, .name = "resize!", .desc = "Resize vector", @@ -549,7 +558,6 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 3, .output_ports = P2_NEXT, .num_outputs = 1, - .kind = NodeKind2::Banged }, { .type_id = NodeTypeID::Cast, @@ -561,10 +569,10 @@ static const NodeType2 NODE_TYPES2[] = { .num_outputs = 1 }, { + .kind = NodeKind2::Special, .type_id = NodeTypeID::Label, .name = "label", .desc = "Text label", - .kind = NodeKind2::Special }, { .type_id = NodeTypeID::Deref, @@ -576,10 +584,10 @@ static const NodeType2 NODE_TYPES2[] = { .num_outputs = 1 }, { + .kind = NodeKind2::Special, .type_id = NodeTypeID::Error, .name = "error", .desc = "Error: invalid node", - .kind = NodeKind2::Special }, }; diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index b8fe3cb..49b418b 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -101,7 +101,7 @@ struct PinMapping { static PinMapping build(const FlowNodeBuilderPtr& node, const NodeType2* nt) { PinMapping m; - m.has_va = nt && nt->va_args != nullptr; + m.has_va = nt && nt->input_ports_va_args != nullptr; int parsed_size = node->parsed_args ? (int)node->parsed_args->size() : 0; // Base args: track which parsed_args indices are ArgNet2 @@ -147,7 +147,7 @@ struct PinMapping { bool is_base(int pin) const { return pin < base_count; } bool is_absent_optional(int pin) const { return pin < base_count && pin_to_port[pin] <= -3000; } int absent_port_index(int pin) const { return -(pin_to_port[pin] + 3000); } - bool is_va(int pin) const { return pin >= base_count && pin < base_count + va_count; } + bool is_input_va(int pin) const { return pin >= base_count && pin < base_count + va_count; } bool is_add_diamond(int pin) const { return pin == add_pin_pos; } bool is_remap(int pin) const { return pin >= base_count + va_count + (has_va ? 1 : 0); } int port_index(int pin) const { return pin_to_port[pin]; } @@ -386,7 +386,7 @@ void Editor2Pane::draw() { if (auto an = (*dst_node->parsed_args)[port]->as_net()) draw_wire_to_pin(i, an->second(), an->first()); } - } else if (dst_pm.is_va(i)) { + } else if (dst_pm.is_input_va(i)) { int va_idx = -(dst_pm.port_index(i) + 1); if (dst_node->parsed_va_args && va_idx < (int)dst_node->parsed_va_args->size()) { if (auto an = (*dst_node->parsed_va_args)[va_idx]->as_net()) @@ -659,7 +659,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, } PortKind2 kind = PortKind2::Data; - bool is_va = pm.is_va(i); + bool is_input_va = pm.is_input_va(i); bool is_optional = false; if (pm.is_absent_optional(i)) { @@ -672,8 +672,8 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, kind = pd->kind; is_optional = pd->optional; } - } else if (is_va) { - kind = nt->va_args ? nt->va_args->kind : PortKind2::Data; + } else if (is_input_va) { + kind = nt->input_ports_va_args ? nt->input_ports_va_args->kind : PortKind2::Data; } // remaps are always Data @@ -682,7 +682,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, dl->AddRectFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y + pr}, pc); } else if (kind == PortKind2::Lambda) { dl->AddTriangleFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y - pr}, {pp.x, pp.y + pr}, pc); - } else if (is_va) { + } else if (is_input_va) { dl->AddQuadFilled({pp.x, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x, pp.y + pr}, {pp.x - pr, pp.y}, pc); } else if (is_optional) { dl->AddQuadFilled({pp.x, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x, pp.y + pr}, {pp.x - pr, pp.y}, pc); @@ -771,7 +771,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, int port = pm.port_index(i); if (node->parsed_args && port < node->parsed_args->size()) pin_arg = (*node->parsed_args)[port]; - } else if (pm.is_va(i)) { + } else if (pm.is_input_va(i)) { int vi = -(pm.port_index(i) + 1); if (node->parsed_va_args && vi < node->parsed_va_args->size()) pin_arg = (*node->parsed_va_args)[vi]; @@ -781,7 +781,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, } if (pin_arg == hovered_pin) { ImVec2 pp = layout.input_pin_pos(i); - auto shape = pm.is_va(i) ? PinShape2::Diamond : PinShape2::Circle; + auto shape = pm.is_input_va(i) ? PinShape2::Diamond : PinShape2::Circle; if (pm.is_base(i)) { if (auto* pd = nt->input_port(pm.port_index(i))) { if (pd->kind == PortKind2::BangTrigger) shape = PinShape2::Square; @@ -949,7 +949,7 @@ Editor2Pane::HoverItem Editor2Pane::detect_hover( int port = pm.port_index(i); if (node->parsed_args && port < node->parsed_args->size()) pin_arg = (*node->parsed_args)[port]; - } else if (pm.is_va(i)) { + } else if (pm.is_input_va(i)) { int vi = -(pm.port_index(i) + 1); if (node->parsed_va_args && vi < node->parsed_va_args->size()) pin_arg = (*node->parsed_va_args)[vi]; From b484ada824688ec3e479cd3102000e7d16e205ab Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 19:37:49 +0200 Subject: [PATCH 66/86] output handling for flow and banged expressions; separate fixed outputs and va_args in editor --- src/atto/graph_builder.cpp | 79 ++++++++++++++++++++++++++++++----- src/attoflow/editor2.cpp | 84 ++++++++++++++++++++++++++++---------- 2 files changed, 132 insertions(+), 31 deletions(-) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index 00bcbe6..1c2da61 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -595,24 +595,69 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // For expr: outputs are all data (no nexts), dynamic count // For others: [nexts..., data_outs..., post_bang] if (is_expr) { - // Expr: positional mapping — all outputs are data, last may be post_bang + // Expr/Expr!: split outputs into fixed (outputs) and va (outputs_va_args) + // Old format: + // Expr (Flow): [out0, out1, ..., post_bang] — post_bang is side-bang → outputs[0] + // Expr! (Banged): [next, out0, out1, ...] — next → outputs[0] + bool is_flow_expr = (nb.type_id == NodeTypeID::Expr); + auto* va_port = new_nt ? new_nt->output_ports_va_args : nullptr; + + // Collect all non-empty, non-lambda entries + std::vector all_outs; + FlowArg2Ptr post_bang_arg = nullptr; for (int i = 0; i < (int)cur_outputs.size(); i++) { auto& net_name = cur_outputs[i]; if (net_name.empty()) continue; - // Check for post_bang suffix - bool is_post_bang = (net_name.size() > 10 && - net_name.compare(net_name.size() - 10, 10, "-post_bang") == 0); if (net_name.size() > 10 && net_name.compare(net_name.size() - 10, 10, "-as_lambda") == 0) continue; + bool is_post_bang = is_flow_expr && (net_name.size() > 10 && + net_name.compare(net_name.size() - 10, 10, "-post_bang") == 0); auto arg = wire_output(net_name); - // post_bang is side-bang for flow nodes — don't add to outputs array - // (it's implicit from NodeKind2::Flow) - if (!is_post_bang) { - while ((int)nb.outputs.size() <= i) - nb.outputs.push_back(gb->build_arg_net("$unconnected", gb->unconnected_net())); - nb.outputs[i] = std::move(arg); + if (is_post_bang) { + // Side-bang for flow expr → goes to outputs[0] + if (new_nt && new_nt->num_outputs > 0) + arg->port(&new_nt->output_ports[0]); + post_bang_arg = std::move(arg); + } else { + if (va_port) arg->port(va_port); + all_outs.push_back(std::move(arg)); } } + + // Populate fixed outputs from descriptor + int fixed_out = new_nt ? new_nt->num_outputs : 0; + nb.outputs.resize(fixed_out); + auto unconnected = gb->unconnected_net(); + + if (is_flow_expr) { + // Flow expr: outputs[0] = side-bang (post_bang or $unconnected) + if (fixed_out > 0) { + nb.outputs[0] = post_bang_arg ? std::move(post_bang_arg) + : gb->build_arg_net("$unconnected", unconnected, + new_nt ? &new_nt->output_ports[0] : nullptr); + } + // All data outputs go to outputs_va_args + for (auto& a : all_outs) + nb.outputs_va_args.push_back(std::move(a)); + } else { + // Banged expr!: outputs[0] = next (first entry from all_outs if it's bang) + if (fixed_out > 0 && !all_outs.empty()) { + all_outs[0]->port(&new_nt->output_ports[0]); + nb.outputs[0] = std::move(all_outs[0]); + // Rest go to outputs_va_args + for (int i = 1; i < (int)all_outs.size(); i++) + nb.outputs_va_args.push_back(std::move(all_outs[i])); + } else { + for (int i = 0; i < fixed_out; i++) + nb.outputs[i] = gb->build_arg_net("$unconnected", unconnected, + &new_nt->output_ports[i]); + } + } + + // Fill va_args to match expression count + int expr_count = nb.parsed_args ? nb.parsed_args->size() : 0; + while ((int)nb.outputs_va_args.size() < expr_count) + nb.outputs_va_args.push_back(gb->build_arg_net("$unconnected", unconnected, va_port)); } else { // Name-based mapping for non-expr nodes int old_num_outs = old_nt ? old_nt->outputs : 0; @@ -644,17 +689,20 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if (new_nt) { nb.outputs.resize(new_nt->num_outputs); auto unconnected = gb->unconnected_net(); + std::set consumed; for (int i = 0; i < new_nt->num_outputs; i++) { auto* pd = &new_nt->output_ports[i]; auto it = out_net_map.find(pd->name); if (it != out_net_map.end()) { it->second->port(pd); nb.outputs[i] = std::move(it->second); + consumed.insert(pd->name); } else if (strcmp(pd->name, "next") == 0) { auto it2 = out_net_map.find("bang"); if (it2 != out_net_map.end()) { it2->second->port(pd); nb.outputs[i] = std::move(it2->second); + consumed.insert("bang"); } else { nb.outputs[i] = gb->build_arg_net("$unconnected", unconnected, pd); } @@ -662,6 +710,15 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { nb.outputs[i] = gb->build_arg_net("$unconnected", unconnected, pd); } } + // Spillover: unconsumed outputs go to outputs_va_args (for event! etc.) + if (new_nt->output_ports_va_args) { + for (auto& [name, arg] : out_net_map) { + if (!consumed.count(name) && name != "post_bang") { + arg->port(new_nt->output_ports_va_args); + nb.outputs_va_args.push_back(std::move(arg)); + } + } + } } } } @@ -1109,6 +1166,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { remap_args(node_p->parsed_va_args.get()); for (auto& r : node_p->remaps) if (auto n = r->as_net()) n->net_id(remap_id(n->first())); for (auto& o : node_p->outputs) if (auto n = o->as_net()) n->net_id(remap_id(n->first())); + for (auto& o : node_p->outputs_va_args) if (auto n = o->as_net()) n->net_id(remap_id(n->first())); } // Rebuild entries map with new keys @@ -1132,6 +1190,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { assign_node(node_p->parsed_va_args.get()); for (auto& r : node_p->remaps) r->node(node_p); for (auto& o : node_p->outputs) o->node(node_p); + for (auto& o : node_p->outputs_va_args) o->node(node_p); } gb->compact(); diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 49b418b..7933f6b 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -97,11 +97,11 @@ struct PinMapping { int base_count = 0; // visible base pins (ArgNet2 in parsed_args + absent optionals) int va_count = 0; // visible va_args pins (ArgNet2 in parsed_va_args) int add_pin_pos = -1; // position of +diamond (-1 if none) - bool has_va = false; + bool has_input_va = false; static PinMapping build(const FlowNodeBuilderPtr& node, const NodeType2* nt) { PinMapping m; - m.has_va = nt && nt->input_ports_va_args != nullptr; + m.has_input_va = nt && nt->input_ports_va_args != nullptr; int parsed_size = node->parsed_args ? (int)node->parsed_args->size() : 0; // Base args: track which parsed_args indices are ArgNet2 @@ -132,7 +132,7 @@ struct PinMapping { } } // +diamond slot - if (m.has_va) { + if (m.has_input_va) { m.add_pin_pos = (int)m.pin_to_port.size(); m.pin_to_port.push_back(-1000); // sentinel for +diamond } @@ -149,7 +149,7 @@ struct PinMapping { int absent_port_index(int pin) const { return -(pin_to_port[pin] + 3000); } bool is_input_va(int pin) const { return pin >= base_count && pin < base_count + va_count; } bool is_add_diamond(int pin) const { return pin == add_pin_pos; } - bool is_remap(int pin) const { return pin >= base_count + va_count + (has_va ? 1 : 0); } + bool is_remap(int pin) const { return pin >= base_count + va_count + (has_input_va ? 1 : 0); } int port_index(int pin) const { return pin_to_port[pin]; } int remap_index(int pin) const { return -(pin_to_port[pin] + 2000); } }; @@ -186,12 +186,11 @@ static NodeLayout compute_node_layout(const FlowNodeBuilderPtr& node, ImVec2 can auto pm = PinMapping::build(node, nt); int num_in = pm.total(); - int num_out = nt ? nt->num_outputs : 1; - if (node->type_id == NodeTypeID::Expr || node->type_id == NodeTypeID::ExprBang) { - int args_count = node->parsed_args ? (int)node->parsed_args->size() : 0; - if (node->type_id == NodeTypeID::ExprBang) num_out = 1 + std::max(1, args_count); - else num_out = std::max(1, args_count); - } + // Output pin count: fixed outputs + output va_args + // For flow nodes, outputs[0] is the side-bang (not rendered at bottom) + int fixed_out = nt ? nt->num_outputs : (int)node->outputs.size(); + int va_out = (int)node->outputs_va_args.size(); + int num_out = fixed_out + va_out; float pin_w_top = std::max(0, num_in) * S.pin_spacing * zoom; float pin_w_bot = std::max(0, num_out) * S.pin_spacing * zoom; @@ -310,6 +309,7 @@ void Editor2Pane::draw() { src_node = src_ptr ? src_ptr->as_Node() : nullptr; if (!src_node) return; named = !net->auto_wire(); + // Search fixed outputs for (int k = 0; k < (int)src_node->outputs.size(); k++) { auto out_net = src_node->outputs[k]->as_net(); if (out_net && out_net->second() == entry) { @@ -317,6 +317,17 @@ void Editor2Pane::draw() { break; } } + // Search output va_args + if (source_pin == 0) { + int base = (int)src_node->outputs.size(); + for (int k = 0; k < (int)src_node->outputs_va_args.size(); k++) { + auto out_net = src_node->outputs_va_args[k]->as_net(); + if (out_net && out_net->second() == entry) { + source_pin = base + k; + break; + } + } + } } else if (auto node = entry->as_Node()) { src_node = node; is_lambda = true; @@ -699,15 +710,24 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, } } - // Draw output pins (bottom) + // Draw output pins (bottom): fixed outputs then va_args outputs + int fixed_out = nt->num_outputs; + int va_out = (int)node->outputs_va_args.size(); for (int i = 0; i < layout.num_out; i++) { ImVec2 pp = layout.output_pin_pos(i); + bool is_output_va = (i >= fixed_out); PortKind2 kind = PortKind2::Data; - if (nt->output_ports && i < nt->num_outputs) kind = nt->output_ports[i].kind; + if (!is_output_va && nt->output_ports && i < nt->num_outputs) + kind = nt->output_ports[i].kind; + else if (is_output_va && nt->output_ports_va_args) + kind = nt->output_ports_va_args->kind; ImU32 pc = pin_color(kind); if (kind == PortKind2::BangNext) { dl->AddRectFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y + pr}, pc); + } else if (is_output_va) { + // Diamond for output va_args (no +diamond button) + dl->AddQuadFilled({pp.x, pp.y + pr}, {pp.x + pr, pp.y}, {pp.x, pp.y - pr}, {pp.x - pr, pp.y}, pc); } else { dl->AddCircleFilled(pp, pr, pc); } @@ -804,12 +824,24 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, return; } } - // Output pins + // Output pins (fixed + va_args) for (int i = 0; i < layout.num_out; i++) { - if (i < (int)node->outputs.size() && node->outputs[i] == hovered_pin) { + FlowArg2Ptr out_pin = nullptr; + bool is_output_va = (i >= fixed_out); + if (!is_output_va && i < (int)node->outputs.size()) + out_pin = node->outputs[i]; + else if (is_output_va && (i - fixed_out) < (int)node->outputs_va_args.size()) + out_pin = node->outputs_va_args[i - fixed_out]; + + if (out_pin == hovered_pin) { ImVec2 pp = layout.output_pin_pos(i); - PortKind2 kind = (nt->output_ports && i < nt->num_outputs) ? nt->output_ports[i].kind : PortKind2::Data; - draw_highlight(pp, kind == PortKind2::BangNext ? PinShape2::Square : PinShape2::Circle); + PinShape2 shape = PinShape2::Circle; + if (!is_output_va && nt->output_ports && i < nt->num_outputs && + nt->output_ports[i].kind == PortKind2::BangNext) + shape = PinShape2::Square; + else if (is_output_va) + shape = PinShape2::Diamond; + draw_highlight(pp, shape); if (draw_tooltips_) { ImGui::BeginTooltip(); ImGui::SetWindowFontScale(S.tooltip_scale); @@ -962,11 +994,21 @@ Editor2Pane::HoverItem Editor2Pane::detect_hover( } } - // Output pins - for (int i = 0; i < layout.num_out; i++) { - float pd = d2(mouse, layout.output_pin_pos(i)); - if (pd < pin_thresh && i < (int)node->outputs.size() && node->outputs[i]) - try_candidate(pd - pin_bias, node->outputs[i]); + // Output pins (fixed + va_args) + { + int n_fixed_out = nt->num_outputs; + for (int i = 0; i < layout.num_out; i++) { + float pd = d2(mouse, layout.output_pin_pos(i)); + if (pd < pin_thresh) { + if (i < n_fixed_out && i < (int)node->outputs.size() && node->outputs[i]) + try_candidate(pd - pin_bias, node->outputs[i]); + else if (i >= n_fixed_out) { + int vi = i - n_fixed_out; + if (vi < (int)node->outputs_va_args.size() && node->outputs_va_args[vi]) + try_candidate(pd - pin_bias, node->outputs_va_args[vi]); + } + } + } } // Lambda grab → node itself (pin-level priority) From 62c1a1dec13b09aa7f53fb38857ba64593521156 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 19:41:03 +0200 Subject: [PATCH 67/86] fix non-bang nodes to have next post bangs --- src/atto/node_types2.h | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/atto/node_types2.h b/src/atto/node_types2.h index a760e0a..8f28da0 100644 --- a/src/atto/node_types2.h +++ b/src/atto/node_types2.h @@ -70,9 +70,7 @@ struct NodeType2 { static const PortDesc2 P2_NEXT[] = { {.name = "next", .desc = "fires after completion", .kind = PortKind2::BangNext, .position = PortPosition2::Output}, }; -static const PortDesc2 P2_RESULT[] = { - {.name = "result", .desc = "result value", .position = PortPosition2::Output}, -}; + static const PortDesc2 P2_NEXT_RESULT[] = { {.name = "next", .desc = "fires after completion", .kind = PortKind2::BangNext, .position = PortPosition2::Output}, {.name = "result", .desc = "result value", .position = PortPosition2::Output}, @@ -253,7 +251,7 @@ static const NodeType2 NODE_TYPES2[] = { .type_id = NodeTypeID::Expr, .name = "expr", .desc = "Evaluate expression", - .output_ports = P2_RESULT, + .output_ports = P2_NEXT, .num_outputs = 1, .output_ports_va_args = &P2_VA_EXPR_OUT, }, @@ -263,7 +261,7 @@ static const NodeType2 NODE_TYPES2[] = { .desc = "Select value by condition", .input_ports = P2_SELECT_IN, .num_inputs = 3, - .output_ports = P2_RESULT, + .output_ports = P2_NEXT_RESULT, .num_outputs = 1 }, { @@ -273,7 +271,7 @@ static const NodeType2 NODE_TYPES2[] = { .input_ports = P2_NEW_IN, .num_inputs = 1, .input_ports_va_args = &P2_VA_FIELD, - .output_ports = P2_RESULT, + .output_ports = P2_NEXT_RESULT, .num_outputs = 1, }, { @@ -282,7 +280,7 @@ static const NodeType2 NODE_TYPES2[] = { .desc = "Duplicate input to output", .input_ports = P2_VALUE, .num_inputs = 1, - .output_ports = P2_RESULT, + .output_ports = P2_NEXT_RESULT, .num_outputs = 1 }, { @@ -291,14 +289,14 @@ static const NodeType2 NODE_TYPES2[] = { .desc = "Convert to string", .input_ports = P2_VALUE, .num_inputs = 1, - .output_ports = P2_RESULT, + .output_ports = P2_NEXT_RESULT, .num_outputs = 1 }, { .type_id = NodeTypeID::Void, .name = "void", .desc = "Void result", - .output_ports = P2_RESULT, + .output_ports = P2_NEXT_RESULT, .num_outputs = 1 }, { @@ -385,7 +383,7 @@ static const NodeType2 NODE_TYPES2[] = { .input_ports = P2_CALL_IN, .num_inputs = 1, .input_ports_va_args = &P2_VA_ARG, - .output_ports = P2_RESULT, + .output_ports = P2_NEXT_RESULT, .num_outputs = 1, }, { @@ -405,7 +403,7 @@ static const NodeType2 NODE_TYPES2[] = { .desc = "Erase from collection", .input_ports = P2_ERASE_IN, .num_inputs = 2, - .output_ports = P2_RESULT, + .output_ports = P2_NEXT_RESULT, .num_outputs = 1 }, { @@ -422,7 +420,7 @@ static const NodeType2 NODE_TYPES2[] = { .desc = "Append to collection", .input_ports = P2_APPEND_IN, .num_inputs = 2, - .output_ports = P2_RESULT, + .output_ports = P2_NEXT_RESULT, .num_outputs = 1 }, { @@ -527,7 +525,7 @@ static const NodeType2 NODE_TYPES2[] = { .desc = "Advance iterator", .input_ports = P2_VALUE, .num_inputs = 1, - .output_ports = P2_RESULT, + .output_ports = P2_NEXT_RESULT, .num_outputs = 1 }, { @@ -565,7 +563,7 @@ static const NodeType2 NODE_TYPES2[] = { .desc = "Cast value to type", .input_ports = P2_VALUE, .num_inputs = 1, - .output_ports = P2_RESULT, + .output_ports = P2_NEXT_RESULT, .num_outputs = 1 }, { @@ -580,7 +578,7 @@ static const NodeType2 NODE_TYPES2[] = { .desc = "Dereference iterator (internal)", .input_ports = P2_VALUE, .num_inputs = 1, - .output_ports = P2_RESULT, + .output_ports = P2_NEXT_RESULT, .num_outputs = 1 }, { From 3389cc378dd7478c9bcc28dfa6f459bf460d6d6e Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 19:54:17 +0200 Subject: [PATCH 68/86] add output ports and num_outputs to node types; adjust flow node handling in editor --- src/atto/node_types2.h | 16 +++++++--- src/attoflow/editor2.cpp | 65 ++++++++++++++++++++++++++-------------- 2 files changed, 54 insertions(+), 27 deletions(-) diff --git a/src/atto/node_types2.h b/src/atto/node_types2.h index 8f28da0..3e42958 100644 --- a/src/atto/node_types2.h +++ b/src/atto/node_types2.h @@ -314,7 +314,9 @@ static const NodeType2 NODE_TYPES2[] = { .name = "discard", .desc = "Discard input values", .input_ports = P2_VALUE, - .num_inputs = 1 + .num_inputs = 1, + .output_ports = P2_NEXT, + .num_outputs = 1 }, { .kind = NodeKind2::Declaration, @@ -438,7 +440,9 @@ static const NodeType2 NODE_TYPES2[] = { .name = "store", .desc = "Store value", .input_ports = P2_STORE_IN, - .num_inputs = 2 + .num_inputs = 2, + .output_ports = P2_NEXT, + .num_outputs = 1 }, { .kind = NodeKind2::Banged, @@ -507,7 +511,9 @@ static const NodeType2 NODE_TYPES2[] = { .name = "iterate", .desc = "Iterate collection", .input_ports = P2_ITERATE_IN, - .num_inputs = 2 + .num_inputs = 2, + .output_ports = P2_NEXT, + .num_outputs = 1 }, { .kind = NodeKind2::Banged, @@ -534,7 +540,9 @@ static const NodeType2 NODE_TYPES2[] = { .desc = "Execute under mutex lock", .input_ports = P2_LOCK_IN, .num_inputs = 2, - .input_ports_va_args = &P2_VA_PARAM + .input_ports_va_args = &P2_VA_PARAM, + .output_ports = P2_NEXT, + .num_outputs = 1, }, { .kind = NodeKind2::Banged, diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 7933f6b..cdbfa7d 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -187,10 +187,16 @@ static NodeLayout compute_node_layout(const FlowNodeBuilderPtr& node, ImVec2 can auto pm = PinMapping::build(node, nt); int num_in = pm.total(); // Output pin count: fixed outputs + output va_args - // For flow nodes, outputs[0] is the side-bang (not rendered at bottom) + // Flow nodes skip outputs[0] (side-bang rendered on the right, not bottom) int fixed_out = nt ? nt->num_outputs : (int)node->outputs.size(); + int skip_side_bang = 0; + if (nt && nt->is_flow()) { + if (node->outputs.empty()) + throw std::logic_error("Flow node '" + node->id() + "' must have at least one output (side-bang)"); + skip_side_bang = 1; + } int va_out = (int)node->outputs_va_args.size(); - int num_out = fixed_out + va_out; + int num_out = std::max(0, fixed_out - skip_side_bang) + va_out; float pin_w_top = std::max(0, num_in) * S.pin_spacing * zoom; float pin_w_bot = std::max(0, num_out) * S.pin_spacing * zoom; @@ -359,7 +365,10 @@ void Editor2Pane::draw() { float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); cp1 = {from.x + dx, from.y}; cp2 = {to.x, to.y - dy}; } else { - from = src_layout.output_pin_pos(source_pin); + // Visual pin index: skip side-bang for flow nodes + int visual_pin = source_pin; + if (src_nt && src_nt->is_flow()) visual_pin = std::max(0, visual_pin - 1); + from = src_layout.output_pin_pos(visual_pin); float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); cp1 = {from.x, from.y + dy}; cp2 = {to.x, to.y - dy}; } @@ -710,15 +719,18 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, } } - // Draw output pins (bottom): fixed outputs then va_args outputs + // Draw output pins (bottom): skip side-bang for flow nodes, then fixed outputs, then va_args + int skip_sb = (nt->is_flow()) ? 1 : 0; int fixed_out = nt->num_outputs; - int va_out = (int)node->outputs_va_args.size(); + int rendered_fixed = fixed_out - skip_sb; // fixed outputs rendered at bottom for (int i = 0; i < layout.num_out; i++) { ImVec2 pp = layout.output_pin_pos(i); - bool is_output_va = (i >= fixed_out); + int out_idx = i + skip_sb; // index into outputs[] (skip side-bang) + bool is_output_va = (i >= rendered_fixed); + PortKind2 kind = PortKind2::Data; - if (!is_output_va && nt->output_ports && i < nt->num_outputs) - kind = nt->output_ports[i].kind; + if (!is_output_va && nt->output_ports && out_idx < nt->num_outputs) + kind = nt->output_ports[out_idx].kind; else if (is_output_va && nt->output_ports_va_args) kind = nt->output_ports_va_args->kind; @@ -726,7 +738,6 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, if (kind == PortKind2::BangNext) { dl->AddRectFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y + pr}, pc); } else if (is_output_va) { - // Diamond for output va_args (no +diamond button) dl->AddQuadFilled({pp.x, pp.y + pr}, {pp.x + pr, pp.y}, {pp.x, pp.y - pr}, {pp.x - pr, pp.y}, pc); } else { dl->AddCircleFilled(pp, pr, pc); @@ -824,20 +835,24 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, return; } } - // Output pins (fixed + va_args) + // Output pins (skip side-bang, then fixed, then va_args) + { + int hl_skip_sb = nt->is_flow() ? 1 : 0; + int hl_rendered_fixed = fixed_out - hl_skip_sb; for (int i = 0; i < layout.num_out; i++) { + int out_idx = i + hl_skip_sb; + bool is_output_va = (i >= hl_rendered_fixed); FlowArg2Ptr out_pin = nullptr; - bool is_output_va = (i >= fixed_out); - if (!is_output_va && i < (int)node->outputs.size()) - out_pin = node->outputs[i]; - else if (is_output_va && (i - fixed_out) < (int)node->outputs_va_args.size()) - out_pin = node->outputs_va_args[i - fixed_out]; + if (!is_output_va && out_idx < (int)node->outputs.size()) + out_pin = node->outputs[out_idx]; + else if (is_output_va && (i - hl_rendered_fixed) < (int)node->outputs_va_args.size()) + out_pin = node->outputs_va_args[i - hl_rendered_fixed]; if (out_pin == hovered_pin) { ImVec2 pp = layout.output_pin_pos(i); PinShape2 shape = PinShape2::Circle; - if (!is_output_va && nt->output_ports && i < nt->num_outputs && - nt->output_ports[i].kind == PortKind2::BangNext) + if (!is_output_va && nt->output_ports && out_idx < nt->num_outputs && + nt->output_ports[out_idx].kind == PortKind2::BangNext) shape = PinShape2::Square; else if (is_output_va) shape = PinShape2::Diamond; @@ -854,6 +869,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, return; } } + } // Side-bang if (nt->is_flow() && !node->outputs.empty() && node->outputs[0] == hovered_pin) { ImVec2 bp = {layout.pos.x + layout.width, layout.pos.y + layout.height * 0.5f}; @@ -994,16 +1010,19 @@ Editor2Pane::HoverItem Editor2Pane::detect_hover( } } - // Output pins (fixed + va_args) + // Output pins (skip side-bang for flow, then fixed, then va_args) { - int n_fixed_out = nt->num_outputs; + int skip_sb = nt->is_flow() ? 1 : 0; + int rendered_fixed = nt->num_outputs - skip_sb; for (int i = 0; i < layout.num_out; i++) { float pd = d2(mouse, layout.output_pin_pos(i)); if (pd < pin_thresh) { - if (i < n_fixed_out && i < (int)node->outputs.size() && node->outputs[i]) - try_candidate(pd - pin_bias, node->outputs[i]); - else if (i >= n_fixed_out) { - int vi = i - n_fixed_out; + int out_idx = i + skip_sb; + bool is_va = (i >= rendered_fixed); + if (!is_va && out_idx < (int)node->outputs.size() && node->outputs[out_idx]) + try_candidate(pd - pin_bias, node->outputs[out_idx]); + else if (is_va) { + int vi = i - rendered_fixed; if (vi < (int)node->outputs_va_args.size() && node->outputs_va_args[vi]) try_candidate(pd - pin_bias, node->outputs_va_args[vi]); } From a4c6917b8ad77ceb887aa18e22bd6d36f610a48a Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 20:08:04 +0200 Subject: [PATCH 69/86] update FlowArg2 name handling and add fq_name method; update editor tooltip display --- src/atto/graph_builder.cpp | 44 ++++++++++++++++++++++++++++++++------ src/atto/graph_builder.h | 7 +++++- src/attoflow/editor2.cpp | 4 ++-- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index 1c2da61..e0c4b0d 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -110,20 +110,26 @@ std::shared_ptr FlowArg2::as_expr() { } std::string FlowArg2::name() const { - std::string prefix = node_->id(); - if (!is_remap()) { - prefix = prefix + "." + port_->name; + std::string prefix = port_->name; if (port_->va_args) { - return prefix + "[" + std::to_string(input_pin_idx()) + "]"; + if (port_->position == PortPosition2::Input) { + return prefix + "[" + std::to_string(input_pin_va_idx()) + "]"; + } else { + return prefix + "[" + std::to_string(output_pin_va_idx()) + "]"; + } } else { return prefix; } } else { - return prefix + ".remaps[" + std::to_string(remap_idx()) + "]"; + return "remaps[" + std::to_string(remap_idx()) + "]"; } } +std::string FlowArg2::fq_name() const { + return node_->id() + "." + name(); +} + unsigned FlowArg2::remap_idx() const { if (!is_remap()) throw std::logic_error("FlowArg2::remap_idx(): not a remap (port is set)"); auto n = node(); @@ -150,6 +156,17 @@ unsigned FlowArg2::input_pin_idx() const { throw std::logic_error("FlowArg2::input_pin_idx(): arg not found in node inputs"); } +unsigned FlowArg2::input_pin_va_idx() const { + if (is_remap()) throw std::logic_error("FlowArg2::input_pin_va_idx(): is a remap (no port)"); + auto n = node(); + auto self = const_cast(this)->shared_from_this(); + if (n->parsed_va_args) { + for (unsigned i = 0; i < (unsigned)n->parsed_va_args->size(); i++) + if ((*n->parsed_va_args)[i] == self) return i; + } + throw std::logic_error("FlowArg2::input_pin_va_idx(): arg not found in node inputs"); +} + unsigned FlowArg2::output_pin_idx() const { if (is_remap()) throw std::logic_error("FlowArg2::output_pin_idx(): is a remap (no port)"); auto n = node(); @@ -159,6 +176,16 @@ unsigned FlowArg2::output_pin_idx() const { throw std::logic_error("FlowArg2::output_pin_idx(): arg not found in node outputs"); } + +unsigned FlowArg2::output_pin_va_idx() const { + if (is_remap()) throw std::logic_error("FlowArg2::output_pin_va_idx(): is a remap (no port)"); + auto n = node(); + auto self = const_cast(this)->shared_from_this(); + for (unsigned i = 0; i < (unsigned)n->outputs_va_args.size(); i++) + if (n->outputs_va_args[i] == self) return i; + throw std::logic_error("FlowArg2::output_pin_va_idx(): arg not found in node outputs"); +} + // ─── Dirty-tracked setters ─── void ArgNet2::net_id(const NodeId& v) { @@ -1107,8 +1134,11 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if ((int)node.parsed_args->size() > fixed_args) { node.parsed_va_args = std::make_shared(); - for (int i = fixed_args; i < (int)node.parsed_args->size(); i++) - node.parsed_va_args->push_back(std::move((*node.parsed_args)[i])); + for (int i = fixed_args; i < (int)node.parsed_args->size(); i++) { + auto arg = std::move((*node.parsed_args)[i]); + arg->port(nt->input_ports_va_args); + node.parsed_va_args->push_back(std::move(arg)); + } node.parsed_args->resize(fixed_args); } } diff --git a/src/atto/graph_builder.h b/src/atto/graph_builder.h index c7ba279..c3cd8d3 100644 --- a/src/atto/graph_builder.h +++ b/src/atto/graph_builder.h @@ -63,12 +63,17 @@ struct FlowArg2 : std::enable_shared_from_this { void port(const PortDesc2* p) { port_ = p; } bool is_remap() const { return port_ == nullptr; } unsigned remap_idx() const; // throws if !is_remap() - unsigned input_pin_idx() const; // throws if is_remap(); looks in parsed_args + parsed_va_args + unsigned input_pin_idx() const; // throws if is_remap(); looks in parsed_args + unsigned input_pin_va_idx() const; // throws if is_remap(); looks in parsed_va_args + unsigned output_pin_idx() const; // throws if is_remap(); looks in outputs + unsigned output_pin_va_idx() const; // throws if is_remap(); looks in outputs_va_args const std::shared_ptr& owner() const; // Computed name: "node.port_name" or "node.va_name[idx]" etc. + std::string fq_name() const; + // only port_name or va_name[idx] std::string name() const; protected: diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index cdbfa7d..32b5154 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -825,7 +825,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, ImGui::BeginTooltip(); ImGui::SetWindowFontScale(S.tooltip_scale); if (hovered_pin->port()) - ImGui::Text("%s", hovered_pin->port()->name); + ImGui::Text("%s", hovered_pin->name().c_str()); else if (pm.is_remap(i)) { int ri = pm.remap_index(i); ImGui::Text("$%d", ri); @@ -861,7 +861,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, ImGui::BeginTooltip(); ImGui::SetWindowFontScale(S.tooltip_scale); if (hovered_pin->port()) - ImGui::Text("%s", hovered_pin->port()->name); + ImGui::Text("%s", hovered_pin->name().c_str()); else ImGui::Text("out%d", i); ImGui::EndTooltip(); From 6c9764004a4bf52187339c50bde2d4185f668295 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 20:13:11 +0200 Subject: [PATCH 70/86] enhance hover functionality to include +diamond pins in editor interactions --- src/attoflow/editor2.cpp | 33 ++++++++++++++++++++++++++++++--- src/attoflow/editor2.h | 9 +++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 32b5154..d4b09a1 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -638,10 +638,12 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, if (auto* ep = std::get_if(&hover_item_)) node_hovered = (*ep == node); - // pin_hovered_on_this_node = hover_item_ is a pin belonging to this node + // pin_hovered_on_this_node = hover_item_ is a pin or +diamond belonging to this node bool pin_hovered_on_this = false; if (auto* pin = std::get_if(&hover_item_)) pin_hovered_on_this = ((*pin)->node() == node); + else if (auto* add = std::get_if(&hover_item_)) + pin_hovered_on_this = (add->node == node); bool has_error = !node->error.empty(); ImU32 col = has_error ? S.col_node_err : (selected ? S.col_node_sel : S.col_node); @@ -792,6 +794,26 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, if (auto* pp = std::get_if(&hover_item_)) hovered_pin = *pp; + // Check if +diamond is hovered + if (auto* add_hover = std::get_if(&hover_item_)) { + if (add_hover->node == node) { + // Find the +diamond position and highlight it + for (int i = 0; i < pm.total(); i++) { + if (pm.is_add_diamond(i)) { + ImVec2 pp = layout.input_pin_pos(i); + draw_highlight(pp, PinShape2::Diamond); + if (draw_tooltips_) { + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + ImGui::Text("add %s", add_hover->va_port ? add_hover->va_port->name : "arg"); + ImGui::EndTooltip(); + } + return; + } + } + } + } + if (hovered_pin) { // Find which visual pin matches and highlight it // Input pins @@ -987,10 +1009,15 @@ Editor2Pane::HoverItem Editor2Pane::detect_hover( float pin_thresh = S.pin_radius * canvas_zoom_ * S.pin_hit_radius_mul; auto pm = PinMapping::build(node, nt); - // Input pins + // Input pins (including +diamond) for (int i = 0; i < pm.total(); i++) { - if (pm.is_add_diamond(i) || pm.is_absent_optional(i)) continue; + if (pm.is_absent_optional(i)) continue; float pd = d2(mouse, layout.input_pin_pos(i)); + if (pm.is_add_diamond(i)) { + if (pd < pin_thresh && nt->input_ports_va_args) + try_candidate(pd - pin_bias, AddPinHover{node, nt->input_ports_va_args, true}); + continue; + } if (pd < pin_thresh) { FlowArg2Ptr pin_arg = nullptr; if (pm.is_base(i)) { diff --git a/src/attoflow/editor2.h b/src/attoflow/editor2.h index 699ac2d..30981ba 100644 --- a/src/attoflow/editor2.h +++ b/src/attoflow/editor2.h @@ -31,8 +31,13 @@ class Editor2Pane { float canvas_zoom_ = 1.0f; // Interaction state - using HoverItem = std::variant; - HoverItem hover_item_; // monostate = nothing, BuilderEntryPtr = node/net, FlowArg2Ptr = pin + struct AddPinHover { + FlowNodeBuilderPtr node; + const PortDesc2* va_port; // the va_args port descriptor template + bool is_input; // true = input +diamond, false = output +diamond (future) + }; + using HoverItem = std::variant; + HoverItem hover_item_; // monostate=nothing, BuilderEntryPtr=node/net, FlowArg2Ptr=pin, AddPinHover=+diamond bool draw_tooltips_ = true; std::set selected_nodes_; bool dragging_started_ = false; From 626cf8b95750ed2601417e862ebdd53aa106e9d7 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 20:16:26 +0200 Subject: [PATCH 71/86] refactor node ID and net name handling; introduce sentinel entries and update pin model --- docs/attolang.md | 104 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 83 insertions(+), 21 deletions(-) diff --git a/docs/attolang.md b/docs/attolang.md index 150b9ff..3517e99 100644 --- a/docs/attolang.md +++ b/docs/attolang.md @@ -671,22 +671,44 @@ Legacy formats (`nanoprog@0`, `nanoprog@1`, `attoprog@0`, `attoprog@1`) are load Node IDs and net names share the same namespace: - Format: `$[a-zA-Z_-][a-zA-Z0-9_-]*` -- Auto-generated: `$auto-` for unnamed entries -- `$0`, `$1`, ... `$N` are reserved for expression pin inputs -- `$unconnected` is a reserved sentinel for unconnected pins +- Auto-generated on import: `$a-0`, `$a-1`, ... `$a-f`, `$a-10`, ... (compact hex, migrated from old `$auto-`) +- `$0`, `$1`, ... `$N` are reserved for expression pin inputs (remaps) +- `$unconnected` is a reserved sentinel net for unconnected pins +- `$empty` is a reserved sentinel node for unassigned pin ownership - The `$` prefix is stored in the file +### Sentinel Entries + +| Sentinel | Type | Purpose | +|---|---|---| +| `$unconnected` | Net | Default wire for unconnected pins | +| `$empty` | Node | Default node owner for pins not yet assigned to a node | + +Both are pre-registered by `GraphBuilder::ensure_sentinels()`. Direct `find()` or `find_or_create_net()` calls with these names throw — use `gb->unconnected_net()` / `gb->empty_node()` instead. + ### Node Structure ```toml [[node]] -id = "$my-node" # human-readable identifier +id = "$a-5" # compact hex identifier type = "store!" # node type name args = ["oscs", "$0"] # inline arguments (expressions, literals, net refs) -remaps = ["$data-net"] # $N → net mapping for expression pin inputs +remaps = ["$a-3-out0"] # $N → net mapping for expression pin inputs position = [100, 200] # canvas coordinates ``` +### Node Kinds + +| Kind | Bang input | Bang output | Side-bang | Description | +|---|---|---|---|---| +| `Flow` | No | Yes (side-bang, right) | Yes | Dataflow node — all flow nodes have a side-bang | +| `Banged` | Yes (top) | Yes (bottom) | No | Imperative node with bang trigger | +| `Event` | No | Yes (bottom) | No | Event source | +| `Declaration` | Yes (top) | Yes (bottom) | No | Compile-time declaration | +| `Special` | No | No | No | Label or Error | + +Flow nodes always have `outputs[0]` as the side-bang (BangNext). It is rendered on the right side, not at the bottom. + ### Arguments (`args`) Each entry in the `args` array is a singular expression (space-delimited in the source, already split in the file). Arguments map 1:1 to the node's descriptor input ports. @@ -697,14 +719,14 @@ An argument can be: - **Number** (`42`, `3.14f`): inline constant — displayed in node text - **String** (`"hello"`): inline string literal — displayed in node text -Only `ArgNet2` (net reference) entries produce visible input pins. Inline values are displayed in the node's label text. +Only net reference entries produce visible input pins. Inline values are displayed in the node's label text. ### Remaps (`remaps`) The `remaps` array maps `$N` expression pin inputs to named nets: ```toml -remaps = ["$iter-item", "$iter-amp"] +remaps = ["$a-2-out0", "$a-2-out1"] ``` - `remaps[0]` = net for `$0`, `remaps[1]` = net for `$1`, etc. @@ -713,6 +735,13 @@ remaps = ["$iter-item", "$iter-amp"] ### Pin Model +Pins are graph entities (`FlowArg2` hierarchy: `ArgNet2`, `ArgNumber2`, `ArgString2`, `ArgExpr2`). Each pin has: +- **`node()`** — owning FlowNodeBuilder (always valid, `$empty` if unassigned) +- **`wire()`** / **`net()`** — associated NetBuilder (always valid, `$unconnected` if unassigned) +- **`port()`** — PortDesc2 descriptor (null for remaps) +- **`name()`** — computed: `"port_name"` or `"va_name[idx]"` or `"remaps[idx]"` +- **`is_remap()`** — true if port is null (remap pin) + #### Input pins (top of node, left to right) Only net reference (`$name`) arguments produce visible pins. The visible pin count is: @@ -720,12 +749,20 @@ Only net reference (`$name`) arguments produce visible pins. The visible pin cou | Section | Visible pins | Source | |---|---|---| | **Base args** | Only net refs in `args` | 1:1 with descriptor input ports | -| **Va-args** | Only net refs in va-args | Named `{template}_0`, `_1`, ... | +| **Input va-args** | Only net refs in va-args | Named `va_name[0]`, `va_name[1]`, ... | +| **+diamond** | Add button (if node has input va-args) | Rendered as ◇ with + | | **Remaps** | All entries | `$0`, `$1`, ... from expressions | #### Output pins (bottom of node) -All descriptor output ports are visible. Exception: `expr`/`expr!` output count equals `args` count. +Fixed descriptor output ports are rendered at the bottom, EXCEPT for flow nodes where `outputs[0]` (side-bang) is rendered on the right side. Output va-args follow fixed outputs. + +| Section | Pins | Source | +|---|---|---| +| **Fixed outputs** | Descriptor output ports (skip side-bang for flow) | `outputs[skip_sb..]` | +| **Output va-args** | Dynamic outputs | `outputs_va_args[]` | + +Expr/expr! have output va-args sized to match expression count. Event! has output va-args for spillover outputs. #### Pin kinds @@ -733,27 +770,52 @@ All descriptor output ports are visible. Exception: `expr`/`expr!` output count |---|---|---| | `BangTrigger` | Square (top) | Trigger input | | `Data` | Circle | Data value | -| `Lambda` | Triangle | Lambda capture (accepts node refs or net refs) | -| `BangNext` | Square (bottom) | Bang continuation output | +| `Lambda` | Down-pointing triangle | Lambda capture (accepts node refs) | +| `BangNext` | Square (bottom/right) | Bang continuation output | +| `Va-args` | Diamond (◇) | Variable-length input/output | +| `Optional` | Diamond with ? | Optional input (trailing) | + +#### Special pins + +| Pin | Position | Visual | Description | +|---|---|---|---| +| **Lambda grab** | Left center | Left-pointing triangle (purple) | Capture this node as lambda | +| **Side-bang** | Right center | Square (yellow) | Post-bang output (flow nodes only) | ### Lambda Captures via Node ID -When a `$id` in an argument resolves to a **node** (not a net), it is a lambda capture. The referenced node's subgraph becomes the lambda body. +When a `$id` in an argument resolves to a **node** (not a net), it is a lambda capture. The wire renders from the source node's lambda grab (left side) to the destination's lambda pin. -- `find_node(id)` → lambda capture -- `find_net(id)` → data wire -- Both `Lambda` and `Data` pins can accept lambda captures -- `Lambda` pins can ONLY accept lambdas (node refs) +- Node reference → lambda capture (wire from grab) +- Net reference → data wire (wire from output pin) +- `Lambda` pins accept only node refs +- `Data` pins can accept either -### Va-args +### Input Va-args -Some node types accept a variable number of additional inputs (e.g., `new` for struct fields, `call` for function arguments, `lock` for lambda parameters). The va-args template is defined on the node type descriptor. Va-args pins are named `{template_name}_0`, `{template_name}_1`, etc. +Some node types accept a variable number of additional inputs. The va-args template is defined on `NodeType2::input_ports_va_args`. | Node | Va-args template | Description | |---|---|---| -| `new` | `field` | Constructor fields (`field_0`, `field_1`, ...) | -| `call` / `call!` | `arg` | Function arguments (`arg_0`, `arg_1`, ...) | -| `lock` / `lock!` | `param` | Lambda parameters (`param_0`, `param_1`, ...) | +| `new` | `field` | Constructor fields (`field[0]`, `field[1]`, ...) | +| `call` / `call!` | `arg` | Function arguments (`arg[0]`, `arg[1]`, ...) | +| `lock` / `lock!` | `param` | Lambda parameters (`param[0]`, `param[1]`, ...) | + +### Output Va-args + +Some node types have dynamic output counts. The template is defined on `NodeType2::output_ports_va_args`. + +| Node | Va-args template | Description | +|---|---|---| +| `expr` | `expr` | One output per expression (`expr[0]`, `expr[1]`, ...) | +| `expr!` | `expr` | Same, after the fixed `next` output | +| `event!` | `args` | Event argument outputs | + +### Optional Ports + +Optional ports are always trailing in the descriptor. They are split into separate `input_optional_ports` / `num_inputs_optional` on NodeType2. If not connected, they are omitted from `parsed_args` (shorter array). The editor shows absent optionals as ◇ with ? inside. + +Currently only `decl_var` has an optional port (`initial`). ### Viewport (Meta File) From 4a7d6c623ebfb890759154d2663e91f6db0ad2de Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 20:20:08 +0200 Subject: [PATCH 72/86] update node types to increase num_outputs from 1 to 2 for multiple nodes --- src/atto/node_types2.h | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/atto/node_types2.h b/src/atto/node_types2.h index 3e42958..b0e8e5e 100644 --- a/src/atto/node_types2.h +++ b/src/atto/node_types2.h @@ -262,7 +262,7 @@ static const NodeType2 NODE_TYPES2[] = { .input_ports = P2_SELECT_IN, .num_inputs = 3, .output_ports = P2_NEXT_RESULT, - .num_outputs = 1 + .num_outputs = 2, }, { .type_id = NodeTypeID::New, @@ -272,7 +272,7 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 1, .input_ports_va_args = &P2_VA_FIELD, .output_ports = P2_NEXT_RESULT, - .num_outputs = 1, + .num_outputs = 2, }, { .type_id = NodeTypeID::Dup, @@ -281,7 +281,7 @@ static const NodeType2 NODE_TYPES2[] = { .input_ports = P2_VALUE, .num_inputs = 1, .output_ports = P2_NEXT_RESULT, - .num_outputs = 1 + .num_outputs = 2, }, { .type_id = NodeTypeID::Str, @@ -290,14 +290,14 @@ static const NodeType2 NODE_TYPES2[] = { .input_ports = P2_VALUE, .num_inputs = 1, .output_ports = P2_NEXT_RESULT, - .num_outputs = 1 + .num_outputs = 2, }, { .type_id = NodeTypeID::Void, .name = "void", .desc = "Void result", .output_ports = P2_NEXT_RESULT, - .num_outputs = 1 + .num_outputs = 2, }, { .kind = NodeKind2::Banged, @@ -386,7 +386,7 @@ static const NodeType2 NODE_TYPES2[] = { .num_inputs = 1, .input_ports_va_args = &P2_VA_ARG, .output_ports = P2_NEXT_RESULT, - .num_outputs = 1, + .num_outputs = 2, }, { .kind = NodeKind2::Banged, @@ -406,7 +406,7 @@ static const NodeType2 NODE_TYPES2[] = { .input_ports = P2_ERASE_IN, .num_inputs = 2, .output_ports = P2_NEXT_RESULT, - .num_outputs = 1 + .num_outputs = 2, }, { .kind = NodeKind2::Banged, @@ -423,7 +423,7 @@ static const NodeType2 NODE_TYPES2[] = { .input_ports = P2_APPEND_IN, .num_inputs = 2, .output_ports = P2_NEXT_RESULT, - .num_outputs = 1 + .num_outputs = 2, }, { .kind = NodeKind2::Banged, @@ -532,7 +532,7 @@ static const NodeType2 NODE_TYPES2[] = { .input_ports = P2_VALUE, .num_inputs = 1, .output_ports = P2_NEXT_RESULT, - .num_outputs = 1 + .num_outputs = 2, }, { .type_id = NodeTypeID::Lock, @@ -572,7 +572,7 @@ static const NodeType2 NODE_TYPES2[] = { .input_ports = P2_VALUE, .num_inputs = 1, .output_ports = P2_NEXT_RESULT, - .num_outputs = 1 + .num_outputs = 2 }, { .kind = NodeKind2::Special, @@ -587,7 +587,7 @@ static const NodeType2 NODE_TYPES2[] = { .input_ports = P2_VALUE, .num_inputs = 1, .output_ports = P2_NEXT_RESULT, - .num_outputs = 1 + .num_outputs = 2, }, { .kind = NodeKind2::Special, From 753c05109d98969f60fe1b060a86b68b0dd483c3 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 21:32:55 +0200 Subject: [PATCH 73/86] as_Node/Net -> as_node/net, dead code deletion --- src/atto/graph_builder.cpp | 40 +++++++++++++++++++------------------- src/atto/graph_builder.h | 4 ++-- src/attoflow/editor2.cpp | 32 +++++++++++++----------------- 3 files changed, 35 insertions(+), 41 deletions(-) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index e0c4b0d..0e05233 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -219,10 +219,10 @@ void ParsedArgs2::set(int i, FlowArg2Ptr arg) { items_[i] = std::move(arg); mayb void BuilderEntry::id(const NodeId& v) { id_ = v; mark_dirty(); } void BuilderEntry::mark_dirty() { maybe_dirty(owner_); } -std::shared_ptr BuilderEntry::as_Node() { +std::shared_ptr BuilderEntry::as_node() { return std::dynamic_pointer_cast(shared_from_this()); } -std::shared_ptr BuilderEntry::as_Net() { +std::shared_ptr BuilderEntry::as_net() { return std::dynamic_pointer_cast(shared_from_this()); } @@ -396,7 +396,7 @@ std::pair GraphBuilder::find_or_create_net(const NodeId throw std::logic_error("find_or_create_net: use unconnected_net()/empty_node() for sentinel '" + name + "'"); auto it = entries.find(name); if (it != entries.end()) { - if (auto net = it->second->as_Net()) { + if (auto net = it->second->as_net()) { if (for_source && !net->source().expired()) throw std::logic_error("find_or_create_net(\"" + name + "\"): net already has a source"); return {it->first, it->second}; @@ -425,18 +425,18 @@ BuilderEntryPtr GraphBuilder::find(const NodeId& id) { FlowNodeBuilderPtr GraphBuilder::find_node(const NodeId& id) { auto it = entries.find(id); if (it == entries.end()) return nullptr; - return it->second->as_Node(); + return it->second->as_node(); } NetBuilderPtr GraphBuilder::find_net(const NodeId& name) { auto it = entries.find(name); if (it == entries.end()) return nullptr; - return it->second->as_Net(); + return it->second->as_net(); } void GraphBuilder::compact() { for (auto it = entries.begin(); it != entries.end(); ) { - if (auto net = it->second->as_Net()) { + if (auto net = it->second->as_net()) { if (!net->is_the_unconnected() && net->unused()) { it = entries.erase(it); continue; @@ -613,7 +613,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // Helper: net a net and return ArgNet2 auto wire_output = [&](const std::string& net_name) -> FlowArg2Ptr { auto [resolved, net_ptr] = gb->find_or_create_net(net_name, true); - if (auto net = net_ptr ? net_ptr->as_Net() : nullptr) + if (auto net = net_ptr ? net_ptr->as_net() : nullptr) net->source(node_entry); return gb->build_arg_net(resolved, net_ptr); }; @@ -771,13 +771,13 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // Try finding as any entry (node or net) auto ptr = gb->find(resolved_name); if (ptr) { - if (auto net = ptr->as_Net()) + if (auto net = ptr->as_net()) net->destinations().push_back(node_entry); return gb->build_arg_net(resolved_name, ptr); } // Not found yet — create as net auto [id, net_ptr] = gb->find_or_create_net(resolved_name); - net_ptr->as_Net()->destinations().push_back(node_entry); + net_ptr->as_net()->destinations().push_back(node_entry); return gb->build_arg_net(id, net_ptr); }; @@ -941,7 +941,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { for (auto& net_name : cur_inputs) { if (net_name.empty()) continue; auto [_, net_ptr] = gb->find_or_create_net(net_name); - if (auto net = net_ptr ? net_ptr->as_Net() : nullptr) + if (auto net = net_ptr ? net_ptr->as_net() : nullptr) net->destinations().push_back(node_entry); } } @@ -1015,14 +1015,14 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { for (auto& a : *pa) { auto an = a->as_net(); if (!an) continue; - if (!an->second() || !an->second()->as_Net()) continue; + if (!an->second() || !an->second()->as_net()) continue; auto actual = gb->find_or_null_node(an->first()); - if (actual && actual->as_Node()) + if (actual && actual->as_node()) an->entry(actual); } }; for (auto& [id, entry] : gb->entries) { - auto node_p = entry->as_Node(); + auto node_p = entry->as_node(); if (!node_p) continue; fixup_args(node_p->parsed_args.get()); fixup_args(node_p->parsed_va_args.get()); @@ -1035,7 +1035,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // Collect shadow ids std::vector shadow_ids; for (auto& [id, entry] : gb->entries) { - auto node_p = entry->as_Node(); + auto node_p = entry->as_node(); if (!node_p) continue; if (node_p->shadow) shadow_ids.push_back(id); @@ -1052,7 +1052,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if (!parent_ptr) continue; auto shadow_entry = gb->find(shadow_id); - auto shadow_ptr = shadow_entry ? shadow_entry->as_Node() : nullptr; + auto shadow_ptr = shadow_entry ? shadow_entry->as_node() : nullptr; if (!shadow_ptr) continue; // Insert shadow expression into parent's parsed_args @@ -1089,7 +1089,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { if (net_ptr) { parent_ptr->remaps[i] = gb->build_arg_net(sin[i], net_ptr); - if (auto net = net_ptr->as_Net()) { + if (auto net = net_ptr->as_net()) { auto& dests = net->destinations(); dests.erase( std::remove_if(dests.begin(), dests.end(), @@ -1109,7 +1109,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // Remove nets where shadow is source (internal shadow→parent plumbing) std::vector nets_to_remove; for (auto& [net_id, net_entry] : gb->entries) { - auto net_as = net_entry->as_Net(); + auto net_as = net_entry->as_net(); if (!net_as) continue; auto src = net_as->source().lock(); if (src == shadow_entry) @@ -1125,7 +1125,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // ─── Split parsed_args into base + va_args for nodes with va_args ─── for (auto& [id, entry] : gb->entries) { if (!entry->is(IdCategory::Node)) continue; - auto& node = *entry->as_Node(); + auto& node = *entry->as_node(); auto* nt = find_node_type2(node.type_id); if (!nt || !nt->input_ports_va_args || !node.parsed_args) continue; @@ -1190,7 +1190,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // Rename all references inside nodes for (auto& [id, entry] : gb->entries) { - auto node_p = entry->as_Node(); + auto node_p = entry->as_node(); if (!node_p) continue; remap_args(node_p->parsed_args.get()); remap_args(node_p->parsed_va_args.get()); @@ -1209,7 +1209,7 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { // ─── Assign node() on all pins ─── for (auto& [id, entry] : gb->entries) { - auto node_p = entry->as_Node(); + auto node_p = entry->as_node(); if (!node_p) continue; auto assign_node = [&](ParsedArgs2* pa) { if (!pa) return; diff --git a/src/atto/graph_builder.h b/src/atto/graph_builder.h index c3cd8d3..1de1239 100644 --- a/src/atto/graph_builder.h +++ b/src/atto/graph_builder.h @@ -209,8 +209,8 @@ struct BuilderEntry: std::enable_shared_from_this { bool is(IdCategory cat) const { return category_ == cat; } - std::shared_ptr as_Node(); - std::shared_ptr as_Net(); + std::shared_ptr as_node(); + std::shared_ptr as_net(); std::shared_ptr owner() const { return owner_; } void owner(const std::shared_ptr& gb) { owner_ = gb; } diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index d4b09a1..0c9cd5d 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -282,7 +282,7 @@ void Editor2Pane::draw() { // Draw nodes (skip shadows) for (auto& [id, entry] : gb_->entries) { - if (auto node = entry->as_Node()) { + if (auto node = entry->as_node()) { if (node->shadow) throw std::logic_error("Editor2Pane: shadow nodes must be folded before rendering (id: " + id + ")"); draw_node(dl, node, canvas_origin); } @@ -292,7 +292,7 @@ void Editor2Pane::draw() { std::vector drawn_wires; // collect for hover testing for (auto& [dst_id, dst_entry] : gb_->entries) { - auto dst_node = dst_entry->as_Node(); + auto dst_node = dst_entry->as_node(); if (!dst_node) continue; auto* dst_nt = find_node_type2(dst_node->type_id); @@ -309,10 +309,10 @@ void Editor2Pane::draw() { bool is_lambda = false; int source_pin = 0; - if (auto net = entry->as_Net()) { + if (auto net = entry->as_net()) { if (net->is_the_unconnected()) return; auto src_ptr = net->source().lock(); - src_node = src_ptr ? src_ptr->as_Node() : nullptr; + src_node = src_ptr ? src_ptr->as_node() : nullptr; if (!src_node) return; named = !net->auto_wire(); // Search fixed outputs @@ -334,7 +334,7 @@ void Editor2Pane::draw() { } } } - } else if (auto node = entry->as_Node()) { + } else if (auto node = entry->as_node()) { src_node = node; is_lambda = true; } else { @@ -436,7 +436,7 @@ void Editor2Pane::draw() { // Extract hover node from variant FlowNodeBuilderPtr hover_node = nullptr; if (auto* ep = std::get_if(&hover_item_)) { - if (*ep) hover_node = (*ep)->as_Node(); + if (*ep) hover_node = (*ep)->as_node(); } else if (auto* pin = std::get_if(&hover_item_)) { hover_node = (*pin)->node(); } @@ -465,7 +465,7 @@ void Editor2Pane::draw() { for (auto& sel : selected_nodes_) { auto sel_layout = compute_node_layout(sel, {0,0}, 1.0f); for (auto& [oid, oe] : gb_->entries) { - auto on = oe->as_Node(); + auto on = oe->as_node(); if (!on || selected_nodes_.count(on)) continue; auto ol = compute_node_layout(on, {0,0}, 1.0f); if (sel->position.x < on->position.x - pad + ol.width + pad * 2 && @@ -502,7 +502,7 @@ void Editor2Pane::draw() { auto sel_layout = compute_node_layout(sel, {0,0}, 1.0f); float nx = sel->position.x + dx, ny = sel->position.y + dy; for (auto& [oid, oe] : gb_->entries) { - auto on = oe->as_Node(); + auto on = oe->as_node(); if (!on || selected_nodes_.count(on)) continue; auto ol = compute_node_layout(on, {0,0}, 1.0f); float ox = on->position.x - pad, oy = on->position.y - pad; @@ -544,7 +544,7 @@ void Editor2Pane::draw() { // Recalculate selection set every frame selected_nodes_.clear(); for (auto& [id, entry] : gb_->entries) { - auto node = entry->as_Node(); + auto node = entry->as_node(); if (!node) continue; auto layout = compute_node_layout(node, {0,0}, 1.0f); float nx0 = node->position.x, ny0 = node->position.y; @@ -975,7 +975,7 @@ Editor2Pane::HoverItem Editor2Pane::detect_hover( // Nodes — distance from mouse to nearest edge of node rect for (auto it = gb_->entries.rbegin(); it != gb_->entries.rend(); ++it) { - auto node = it->second->as_Node(); + auto node = it->second->as_node(); if (!node) continue; auto layout = compute_node_layout(node, canvas_origin, canvas_zoom_); @@ -1001,7 +1001,7 @@ Editor2Pane::HoverItem Editor2Pane::detect_hover( // Pins — check all nodes, find closest pin globally for (auto it = gb_->entries.rbegin(); it != gb_->entries.rend(); ++it) { - auto node = it->second->as_Node(); + auto node = it->second->as_node(); if (!node) continue; auto* nt = find_node_type2(node->type_id); if (!nt) continue; @@ -1096,7 +1096,7 @@ void Editor2Pane::draw_hover_effects( if (auto* ep = std::get_if(&hover)) { hover_entry = *ep; - hover_node = hover_entry ? hover_entry->as_Node() : nullptr; + hover_node = hover_entry ? hover_entry->as_node() : nullptr; } else if (auto* pp = std::get_if(&hover)) { hover_pin = *pp; } @@ -1110,7 +1110,7 @@ void Editor2Pane::draw_hover_effects( } // Wire/net hovered: highlight all wires in the same net + lambda source node - if (hover_entry && hover_entry->as_Net()) { + if (hover_entry && hover_entry->as_net()) { for (auto& w : drawn_wires) { if (w.entry() == hover_entry) dl->AddBezierCubic(w.p0, w.p1, w.p2, w.p3, S.col_pin_hover, th_wire); @@ -1133,10 +1133,4 @@ void Editor2Pane::draw_hover_effects( } } } - - // Lambda node hovered via wire: highlight the source node - if (hover_entry && hover_entry->as_Node() && !hover_node) { - // This case doesn't happen — if entry is a node, hover_node is set. - // Lambda source highlighting is handled above. - } } From 1ae47782509895d1e5f7f2c0579c2228e364165b Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 21:53:15 +0200 Subject: [PATCH 74/86] Observer Pattern --- src/atto/graph_builder.cpp | 247 ++++++++++++- src/atto/graph_builder.h | 36 +- src/atto/graph_editor_interfaces.h | 73 ++++ src/attoflow/editor.cpp | 9 +- src/attoflow/editor.h | 2 +- src/attoflow/editor2.cpp | 552 +++++++++++++---------------- src/attoflow/editor2.h | 203 ++++++++++- 7 files changed, 802 insertions(+), 320 deletions(-) create mode 100644 src/atto/graph_editor_interfaces.h diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index 0e05233..4e460d9 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -71,7 +71,49 @@ FlowArg2::FlowArg2(ArgKind kind, const std::shared_ptr& owner) if (!net_) throw std::logic_error("FlowArg2: net must not be null"); } -void FlowArg2::mark_dirty() { maybe_dirty(owner_); } +void FlowArg2::mark_dirty() { + maybe_dirty(owner_); + // Only enqueue editor callbacks if editors are registered + if (!owner_->has_editors()) return; + // Enqueue type-specific arg editor callbacks + switch (kind_) { + case ArgKind::Net: { + auto self = std::dynamic_pointer_cast(shared_from_this()); + owner_->add_mutation_call(this, [self]() { + for (auto& we : self->editors_) + if (auto e = we.lock()) e->arg_net_mutated(self); + }); + break; + } + case ArgKind::Number: { + auto self = std::dynamic_pointer_cast(shared_from_this()); + owner_->add_mutation_call(this, [self]() { + for (auto& we : self->editors_) + if (auto e = we.lock()) e->arg_number_mutated(self); + }); + break; + } + case ArgKind::String: { + auto self = std::dynamic_pointer_cast(shared_from_this()); + owner_->add_mutation_call(this, [self]() { + for (auto& we : self->editors_) + if (auto e = we.lock()) e->arg_string_mutated(self); + }); + break; + } + case ArgKind::Expr: { + auto self = std::dynamic_pointer_cast(shared_from_this()); + owner_->add_mutation_call(this, [self]() { + for (auto& we : self->editors_) + if (auto e = we.lock()) e->arg_expr_mutated(self); + }); + break; + } + } + // Bubble up to node (structural change) + if (node_ && !node_->is_the_empty) + node_->mark_dirty(); +} const FlowNodeBuilderPtr& FlowArg2::node() const { if (!node_) throw std::logic_error("FlowArg2::node(): node is null"); @@ -217,7 +259,23 @@ void ParsedArgs2::set(int i, FlowArg2Ptr arg) { items_[i] = std::move(arg); mayb // ─── BuilderEntry ─── void BuilderEntry::id(const NodeId& v) { id_ = v; mark_dirty(); } -void BuilderEntry::mark_dirty() { maybe_dirty(owner_); } +void BuilderEntry::mark_dirty() { + maybe_dirty(owner_); + if (!owner_ || !owner_->has_editors()) return; + if (is(IdCategory::Node)) { + auto self = std::dynamic_pointer_cast(shared_from_this()); + owner_->add_mutation_call(this, [self]() { + for (auto& we : self->editors_) + if (auto e = we.lock()) e->node_mutated(self); + }); + } else if (is(IdCategory::Net)) { + auto self = std::dynamic_pointer_cast(shared_from_this()); + owner_->add_mutation_call(this, [self]() { + for (auto& we : self->editors_) + if (auto e = we.lock()) e->net_mutated(self); + }); + } +} std::shared_ptr BuilderEntry::as_node() { return std::dynamic_pointer_cast(shared_from_this()); @@ -491,6 +549,191 @@ FlowArg2Ptr GraphBuilder::build_arg_expr(std::string expr, const PortDesc2* port return p; } +// ─── Mutation batching ─── + +void GraphBuilder::edit_start() { + if (!mutations_.empty()) + throw std::logic_error("GraphBuilder::edit_start(): previous edit_commit() was missed (" + + std::to_string(mutations_.size()) + " pending mutations)"); +} + +void GraphBuilder::edit_commit() { + auto mutations = std::move(mutations_); + mutations_.clear(); + mutation_items_.clear(); + for (auto& fn : mutations) + fn(); +} + +void GraphBuilder::add_mutation_call(void* ptr, std::function&& fn) { + if (mutation_items_.count(ptr)) return; + mutation_items_.insert(ptr); + mutations_.push_back(std::move(fn)); +} + +// ─── Editor registration ─── + +// Helper: create arg editors for all args belonging to a node +static void register_arg_editors(const std::shared_ptr& node_editor, + const FlowNodeBuilderPtr& node) { + // Helper lambda to process a single arg + auto process_arg = [&](const FlowArg2Ptr& arg) { + if (!arg) return; + switch (arg->kind()) { + case ArgKind::Net: { + auto a = arg->as_net(); + auto ed = node_editor->create_arg_net_editor(a); + if (ed) a->editors_.push_back(ed); + break; + } + case ArgKind::Number: { + auto a = arg->as_number(); + auto ed = node_editor->create_arg_number_editor(a); + if (ed) a->editors_.push_back(ed); + break; + } + case ArgKind::String: { + auto a = arg->as_string(); + auto ed = node_editor->create_arg_string_editor(a); + if (ed) a->editors_.push_back(ed); + break; + } + case ArgKind::Expr: { + auto a = arg->as_expr(); + auto ed = node_editor->create_arg_expr_editor(a); + if (ed) a->editors_.push_back(ed); + break; + } + } + }; + + // Process all arg containers on the node + if (node->parsed_args) { + for (int i = 0; i < node->parsed_args->size(); i++) + process_arg((*node->parsed_args)[i]); + } + if (node->parsed_va_args) { + for (int i = 0; i < node->parsed_va_args->size(); i++) + process_arg((*node->parsed_va_args)[i]); + } + for (auto& arg : node->remaps) process_arg(arg); + for (auto& arg : node->outputs) process_arg(arg); + for (auto& arg : node->outputs_va_args) process_arg(arg); +} + +// Helper: enqueue initial mutated callbacks bottom-up for a node and its args +static void enqueue_initial_mutations(GraphBuilder* gb, const FlowNodeBuilderPtr& node) { + // Enqueue arg mutations first (innermost) + auto enqueue_arg = [&](const FlowArg2Ptr& arg) { + if (!arg) return; + switch (arg->kind()) { + case ArgKind::Net: { + auto a = arg->as_net(); + gb->add_mutation_call(a.get(), [a]() { + for (auto& we : a->editors_) + if (auto e = we.lock()) e->arg_net_mutated(a); + }); + break; + } + case ArgKind::Number: { + auto a = arg->as_number(); + gb->add_mutation_call(a.get(), [a]() { + for (auto& we : a->editors_) + if (auto e = we.lock()) e->arg_number_mutated(a); + }); + break; + } + case ArgKind::String: { + auto a = arg->as_string(); + gb->add_mutation_call(a.get(), [a]() { + for (auto& we : a->editors_) + if (auto e = we.lock()) e->arg_string_mutated(a); + }); + break; + } + case ArgKind::Expr: { + auto a = arg->as_expr(); + gb->add_mutation_call(a.get(), [a]() { + for (auto& we : a->editors_) + if (auto e = we.lock()) e->arg_expr_mutated(a); + }); + break; + } + } + }; + + if (node->parsed_args) + for (int i = 0; i < node->parsed_args->size(); i++) + enqueue_arg((*node->parsed_args)[i]); + if (node->parsed_va_args) + for (int i = 0; i < node->parsed_va_args->size(); i++) + enqueue_arg((*node->parsed_va_args)[i]); + for (auto& a : node->remaps) enqueue_arg(a); + for (auto& a : node->outputs) enqueue_arg(a); + for (auto& a : node->outputs_va_args) enqueue_arg(a); + + // Then enqueue node mutation (outer) + gb->add_mutation_call(node.get(), [node]() { + for (auto& we : node->editors_) + if (auto e = we.lock()) e->node_mutated(node); + }); +} + +void GraphBuilder::add_editor(const std::shared_ptr& editor) { + editors_.push_back(editor); + + // Register existing entries and fire initial mutations + edit_start(); + + for (auto& [id, entry] : entries) { + if (auto node = entry->as_node()) { + if (node->shadow || node->is_the_empty) continue; + auto node_ed = editor->node_added(id, node); + if (node_ed) { + node->editors_.push_back(node_ed); + register_arg_editors(node_ed, node); + enqueue_initial_mutations(this, node); + } + } else if (auto net = entry->as_net()) { + if (net->is_the_unconnected()) continue; + auto net_ed = editor->net_added(id, net); + if (net_ed) { + net->editors_.push_back(net_ed); + add_mutation_call(net.get(), [net]() { + for (auto& we : net->editors_) + if (auto e = we.lock()) e->net_mutated(net); + }); + } + } + } + + edit_commit(); +} + +void GraphBuilder::remove_editor(const std::shared_ptr& editor) { + editors_.erase( + std::remove_if(editors_.begin(), editors_.end(), + [&](const std::weak_ptr& w) { + auto s = w.lock(); + return !s || s == editor; + }), + editors_.end()); + // Note: per-item editor weak_ptrs will naturally expire +} + +// ─── FlowNodeBuilder::mark_layout_dirty ─── + +void FlowNodeBuilder::mark_layout_dirty() { + auto gb = owner(); + if (gb) gb->mark_dirty(); + if (!gb || !gb->has_editors()) return; + auto self = std::dynamic_pointer_cast(shared_from_this()); + gb->add_mutation_call(this, [self]() { + for (auto& we : self->editors_) + if (auto e = we.lock()) e->node_layout_changed(self); + }); +} + // ─── Deserializer ─── BuilderResult Deserializer::parse_node( diff --git a/src/atto/graph_builder.h b/src/atto/graph_builder.h index 1de1239..2c10b7e 100644 --- a/src/atto/graph_builder.h +++ b/src/atto/graph_builder.h @@ -2,6 +2,7 @@ #include "model.h" #include "types.h" #include "node_types.h" +#include "graph_editor_interfaces.h" #include #include #include @@ -10,6 +11,8 @@ #include #include #include +#include +#include #include using NodeId = std::string; @@ -115,6 +118,8 @@ struct ArgNet2 : FlowArg2 { NodeId net_id_; std::shared_ptr entry_; +public: + std::vector> editors_; }; struct ArgNumber2 : FlowArg2 { @@ -132,6 +137,8 @@ struct ArgNumber2 : FlowArg2 { double value_ = 0; bool is_float_ = false; +public: + std::vector> editors_; }; struct ArgString2 : FlowArg2 { @@ -145,6 +152,8 @@ struct ArgString2 : FlowArg2 { : FlowArg2(ArgKind::String, owner), value_(std::move(v)) {} std::string value_; +public: + std::vector> editors_; }; struct ArgExpr2 : FlowArg2 { @@ -158,6 +167,8 @@ struct ArgExpr2 : FlowArg2 { : FlowArg2(ArgKind::Expr, owner), expr_(std::move(e)) {} std::string expr_; +public: + std::vector> editors_; }; // ─── ParsedArgs2: vector of FlowArg2Ptr with dirty tracking ─── @@ -215,7 +226,6 @@ struct BuilderEntry: std::enable_shared_from_this { std::shared_ptr owner() const { return owner_; } void owner(const std::shared_ptr& gb) { owner_ = gb; } -protected: void mark_dirty(); private: @@ -253,6 +263,8 @@ struct NetBuilder: BuilderEntry { bool is_the_unconnected_ = false; BuilderEntryWeak source_; std::vector destinations_; +public: + std::vector> editors_; }; // ─── FlowNodeBuilder ─── @@ -275,6 +287,11 @@ struct FlowNodeBuilder: BuilderEntry { std::string error; std::string args_str() const; + + // Layout-only dirty (position changed). Does NOT bubble to args or graph-level. + void mark_layout_dirty(); + + std::vector> editors_; }; using BuilderResult = std::variant, BuilderError>; @@ -320,11 +337,28 @@ struct GraphBuilder : std::enable_shared_from_this { void mark_dirty() { dirty_ = true; } bool is_dirty() { return dirty_; } + // Editor registration + void add_editor(const std::shared_ptr& editor); + void remove_editor(const std::shared_ptr& editor); + + // Mutation batching + void edit_start(); // throws if mutations_ not empty (missed commit) + void edit_commit(); // fires all queued callbacks in insertion order, then clears + void add_mutation_call(void* ptr, std::function&& fn); + bool has_editors() const { return !editors_.empty(); } + private: bool dirty_ = false; std::vector pins_; FlowNodeBuilderPtr empty_; NetBuilderPtr unconnected_; + + // Editor observers + std::vector> editors_; + + // Mutation batch (between edit_start/edit_commit) + std::vector> mutations_; + std::set mutation_items_; }; // ─── Parse/reconstruct helpers ─── diff --git a/src/atto/graph_editor_interfaces.h b/src/atto/graph_editor_interfaces.h new file mode 100644 index 0000000..d48bfe3 --- /dev/null +++ b/src/atto/graph_editor_interfaces.h @@ -0,0 +1,73 @@ +#pragma once +#include +#include + +// Forward declarations — no graph_builder.h dependency +struct FlowNodeBuilder; +struct NetBuilder; +struct ArgNet2; +struct ArgNumber2; +struct ArgString2; +struct ArgExpr2; +using NodeId = std::string; + +// ─── Arg editor interfaces (per-type) ─── + +struct IArgNetEditor { + virtual ~IArgNetEditor() = default; + virtual void arg_net_mutated(const std::shared_ptr& arg) = 0; +}; + +struct IArgNumberEditor { + virtual ~IArgNumberEditor() = default; + virtual void arg_number_mutated(const std::shared_ptr& arg) = 0; +}; + +struct IArgStringEditor { + virtual ~IArgStringEditor() = default; + virtual void arg_string_mutated(const std::shared_ptr& arg) = 0; +}; + +struct IArgExprEditor { + virtual ~IArgExprEditor() = default; + virtual void arg_expr_mutated(const std::shared_ptr& arg) = 0; +}; + +// ─── Node editor ─── + +struct INodeEditor { + virtual ~INodeEditor() = default; + + // Structural change: args, ports, connections changed + virtual void node_mutated(const std::shared_ptr& node) = 0; + + // Visual-only change: position moved (does NOT bubble up) + virtual void node_layout_changed(const std::shared_ptr& node) = 0; + + // Arg editor factories — called per-arg when node is registered + virtual std::shared_ptr create_arg_net_editor(const std::shared_ptr& arg) = 0; + virtual std::shared_ptr create_arg_number_editor(const std::shared_ptr& arg) = 0; + virtual std::shared_ptr create_arg_string_editor(const std::shared_ptr& arg) = 0; + virtual std::shared_ptr create_arg_expr_editor(const std::shared_ptr& arg) = 0; +}; + +// ─── Net editor ─── + +struct INetEditor { + virtual ~INetEditor() = default; + virtual void net_mutated(const std::shared_ptr& net) = 0; +}; + +// ─── Graph editor (top-level observer) ─── + +struct IGraphEditor { + virtual ~IGraphEditor() = default; + + // Node lifecycle — returns per-node editor to attach + virtual std::shared_ptr node_added(const NodeId& id, const std::shared_ptr& node) = 0; + virtual void node_removed(const NodeId& id) = 0; + + // Net lifecycle — returns per-net editor to attach + virtual std::shared_ptr net_added(const NodeId& id, const std::shared_ptr& net) = 0; + virtual void net_removed(const NodeId& id) = 0; +}; diff --git a/src/attoflow/editor.cpp b/src/attoflow/editor.cpp index dc5bbed..63706a7 100644 --- a/src/attoflow/editor.cpp +++ b/src/attoflow/editor.cpp @@ -325,10 +325,11 @@ void FlowEditorWindow::open_tab(const std::string& file_path) { tab.file_path = abs_path; tab.tab_name = fs::path(file_path).stem().string(); tab.use_editor2 = true; + tab.editor2 = std::make_shared(); if (fs::exists(abs_path)) { // Try loading via Editor2Pane first - if (!tab.editor2.load(abs_path)) { + if (!tab.editor2->load(abs_path)) { // Fallback to legacy loader tab.use_editor2 = false; load_atto(abs_path, tab.graph); @@ -862,10 +863,10 @@ void FlowEditorWindow::draw() { if (ImGui::BeginTabBar("##atto_tabs")) { for (int i = 0; i < (int)tabs_.size(); i++) { std::string label = tabs_[i].use_editor2 - ? tabs_[i].editor2.tab_name() + ? tabs_[i].editor2->tab_name() : tabs_[i].tab_name; bool tab_dirty = tabs_[i].use_editor2 - ? tabs_[i].editor2.is_dirty() + ? tabs_[i].editor2->is_dirty() : tabs_[i].dirty; if (tab_dirty) label += "*"; label += "###tab" + std::to_string(i); @@ -900,7 +901,7 @@ void FlowEditorWindow::draw() { ImGuiWindowFlags_NoScrollbar); if (active().use_editor2) { - active().editor2.draw(); + active().editor2->draw(); ImGui::EndChild(); // flow_canvas } else { // === Legacy Editor1 canvas === diff --git a/src/attoflow/editor.h b/src/attoflow/editor.h index 67f1dc4..684fcc0 100644 --- a/src/attoflow/editor.h +++ b/src/attoflow/editor.h @@ -21,7 +21,7 @@ inline Vec2 to_vec2(ImVec2 v) { return {v.x, v.y}; } // Per-tab state: each open .atto file gets its own TabState struct TabState { FlowGraph graph; // legacy (Editor1) - Editor2Pane editor2; // new editor pane + std::shared_ptr editor2; // new editor pane bool use_editor2 = true; // true = use Editor2Pane, false = legacy std::string file_path; // absolute path to this .atto file std::string tab_name; // display name (filename without extension) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 0c9cd5d..1c3cb58 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -9,64 +9,9 @@ #include #include -// ─── Style ─── - -static struct { - // Layout - float node_min_width = 80.0f; - float node_height = 40.0f; - float pin_radius = 5.0f; - float pin_spacing = 16.0f; - float node_rounding = 4.0f; - float grid_step = 20.0f; - - // Thickness - float wire_thickness = 2.5f; - float node_border = 1.0f; // implicit from AddRect - float highlight_offset = 2.0f; - float highlight_thickness = 2.0f; - float add_pin_line = 1.5f; - - // Hit testing - float pin_hit_radius_mul = 2.5f; - float wire_hit_threshold = 30.0f; - float node_hit_threshold_mul = 6.f; // multiplied by pin_radius * zoom - float dismiss_radius = 20.0f; - float pin_priority_bias = 1e6f; // pins always win over nodes/wires when within threshold - - // Canvas colors - ImU32 col_bg = IM_COL32(30, 30, 40, 255); - ImU32 col_grid = IM_COL32(50, 50, 60, 255); - - // Node colors - ImU32 col_node = IM_COL32(50, 55, 75, 220); - ImU32 col_node_sel = IM_COL32(80, 90, 130, 255); - ImU32 col_node_err = IM_COL32(130, 40, 40, 220); - ImU32 col_node_border = IM_COL32(80, 80, 100, 255); - ImU32 col_err_border = IM_COL32(255, 80, 80, 255); - ImU32 col_text = IM_COL32(220, 220, 220, 255); - - // Pin colors - ImU32 col_pin_data = IM_COL32(100, 200, 100, 255); - ImU32 col_pin_bang = IM_COL32(255, 200, 80, 255); - ImU32 col_pin_lambda = IM_COL32(180, 130, 255, 255); - ImU32 col_pin_hover = IM_COL32(255, 255, 255, 255); - ImU32 col_add_pin = IM_COL32(120, 120, 140, 180); - ImU32 col_add_pin_fg = IM_COL32(200, 200, 220, 220); - ImU32 col_opt_pin_fg = IM_COL32(30, 30, 40, 255); - - // Wire colors - ImU32 col_wire = IM_COL32(200, 200, 100, 200); - ImU32 col_wire_named = IM_COL32(200, 200, 100, 120); - ImU32 col_wire_lambda = IM_COL32(180, 130, 255, 200); - - // Net label colors - ImU32 col_label_bg = IM_COL32(30, 30, 40, 200); - ImU32 col_label_text = IM_COL32(180, 220, 255, 255); - - // Tooltip - float tooltip_scale = 1.0f; -} S; +// ─── Style (global instance) ─── + +Editor2Style S; // ─── Helpers ─── @@ -84,97 +29,54 @@ static float point_to_bezier_dist(ImVec2 p, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImV return std::sqrt(min_d2); } -// WireInfo defined in editor2.h - static inline ImVec2 v2add(ImVec2 a, ImVec2 b) { return {a.x + b.x, a.y + b.y}; } static inline ImVec2 v2sub(ImVec2 a, ImVec2 b) { return {a.x - b.x, a.y - b.y}; } static inline ImVec2 v2mul(ImVec2 a, float s) { return {a.x * s, a.y * s}; } -// Maps visible pin index → descriptor port index for input pins -// Sections: base_args ArgNet2, va_args ArgNet2, [+diamond], remaps -struct PinMapping { - std::vector pin_to_port; // visible pin idx → port index in parsed_args/descriptor - int base_count = 0; // visible base pins (ArgNet2 in parsed_args + absent optionals) - int va_count = 0; // visible va_args pins (ArgNet2 in parsed_va_args) - int add_pin_pos = -1; // position of +diamond (-1 if none) - bool has_input_va = false; - - static PinMapping build(const FlowNodeBuilderPtr& node, const NodeType2* nt) { - PinMapping m; - m.has_input_va = nt && nt->input_ports_va_args != nullptr; - int parsed_size = node->parsed_args ? (int)node->parsed_args->size() : 0; - - // Base args: track which parsed_args indices are ArgNet2 - if (node->parsed_args) { - for (int i = 0; i < parsed_size; i++) { - if ((*node->parsed_args)[i]->is(ArgKind::Net)) { - m.pin_to_port.push_back(i); - m.base_count++; - } +// ─── PinMapping::build ─── + +PinMapping PinMapping::build(const FlowNodeBuilderPtr& node, const NodeType2* nt) { + PinMapping m; + m.has_input_va = nt && nt->input_ports_va_args != nullptr; + int parsed_size = node->parsed_args ? (int)node->parsed_args->size() : 0; + + if (node->parsed_args) { + for (int i = 0; i < parsed_size; i++) { + if ((*node->parsed_args)[i]->is(ArgKind::Net)) { + m.pin_to_port.push_back(i); + m.base_count++; } } - // Absent trailing optional ports: show as pins beyond parsed_args - if (nt) { - for (int i = parsed_size; i < nt->total_inputs(); i++) { - if (i >= nt->num_inputs) { // port is in the optional range - m.pin_to_port.push_back(-3000 - i); - m.base_count++; - } + } + if (nt) { + for (int i = parsed_size; i < nt->total_inputs(); i++) { + if (i >= nt->num_inputs) { + m.pin_to_port.push_back(-3000 - i); + m.base_count++; } } - // Va_args - if (node->parsed_va_args) { - for (int i = 0; i < (int)node->parsed_va_args->size(); i++) { - if ((*node->parsed_va_args)[i]->is(ArgKind::Net)) { - m.pin_to_port.push_back(-(i + 1)); // negative = va_args index (1-based) - m.va_count++; - } + } + if (node->parsed_va_args) { + for (int i = 0; i < (int)node->parsed_va_args->size(); i++) { + if ((*node->parsed_va_args)[i]->is(ArgKind::Net)) { + m.pin_to_port.push_back(-(i + 1)); + m.va_count++; } } - // +diamond slot - if (m.has_input_va) { - m.add_pin_pos = (int)m.pin_to_port.size(); - m.pin_to_port.push_back(-1000); // sentinel for +diamond - } - // Remaps - for (int i = 0; i < (int)node->remaps.size(); i++) { - m.pin_to_port.push_back(-2000 - i); // sentinel for remap i - } - return m; } - - int total() const { return (int)pin_to_port.size(); } - bool is_base(int pin) const { return pin < base_count; } - bool is_absent_optional(int pin) const { return pin < base_count && pin_to_port[pin] <= -3000; } - int absent_port_index(int pin) const { return -(pin_to_port[pin] + 3000); } - bool is_input_va(int pin) const { return pin >= base_count && pin < base_count + va_count; } - bool is_add_diamond(int pin) const { return pin == add_pin_pos; } - bool is_remap(int pin) const { return pin >= base_count + va_count + (has_input_va ? 1 : 0); } - int port_index(int pin) const { return pin_to_port[pin]; } - int remap_index(int pin) const { return -(pin_to_port[pin] + 2000); } -}; - -// Computed node layout for drawing -struct NodeLayout { - ImVec2 pos; // top-left screen position - float width; - float height; - int num_in; - int num_out; - float zoom; - - ImVec2 input_pin_pos(int i) const { - return {pos.x + (i + 0.5f) * S.pin_spacing * zoom, pos.y}; + if (m.has_input_va) { + m.add_pin_pos = (int)m.pin_to_port.size(); + m.pin_to_port.push_back(-1000); } - ImVec2 output_pin_pos(int i) const { - return {pos.x + (i + 0.5f) * S.pin_spacing * zoom, pos.y + height}; + for (int i = 0; i < (int)node->remaps.size(); i++) { + m.pin_to_port.push_back(-2000 - i); } - ImVec2 lambda_grab_pos() const { - return {pos.x, pos.y + height * 0.5f}; - } -}; + return m; +} -static NodeLayout compute_node_layout(const FlowNodeBuilderPtr& node, ImVec2 canvas_origin, float zoom) { +// ─── compute_node_layout ─── + +NodeLayout compute_node_layout(const FlowNodeBuilderPtr& node, ImVec2 canvas_origin, float zoom) { auto* nt = find_node_type2(node->type_id); std::string display = nt ? nt->name : "?"; std::string args = node->args_str(); @@ -186,8 +88,7 @@ static NodeLayout compute_node_layout(const FlowNodeBuilderPtr& node, ImVec2 can auto pm = PinMapping::build(node, nt); int num_in = pm.total(); - // Output pin count: fixed outputs + output va_args - // Flow nodes skip outputs[0] (side-bang rendered on the right, not bottom) + int fixed_out = nt ? nt->num_outputs : (int)node->outputs.size(); int skip_side_bang = 0; if (nt && nt->is_flow()) { @@ -218,6 +119,75 @@ static ImU32 pin_color(PortKind2 kind) { } } +// ─── Per-item editor implementations ─── + +void NodeEditorImpl::rebuild(ImVec2 canvas_origin, float zoom) { + auto* nt = find_node_type2(node->type_id); + layout = compute_node_layout(node, canvas_origin, zoom); + pin_mapping = PinMapping::build(node, nt); + display_text = nt ? nt->name : "?"; + std::string args = node->args_str(); + if (!args.empty()) display_text += " " + args; + has_error = !node->error.empty(); +} + +void NodeEditorImpl::node_mutated(const std::shared_ptr&) { + pane->invalidate_wires(); +} + +void NodeEditorImpl::node_layout_changed(const std::shared_ptr&) { + pane->invalidate_wires(); +} + +std::shared_ptr NodeEditorImpl::create_arg_net_editor(const std::shared_ptr& arg) { + return std::make_shared(pane, arg); +} +std::shared_ptr NodeEditorImpl::create_arg_number_editor(const std::shared_ptr& arg) { + return std::make_shared(pane, arg); +} +std::shared_ptr NodeEditorImpl::create_arg_string_editor(const std::shared_ptr& arg) { + return std::make_shared(pane, arg); +} +std::shared_ptr NodeEditorImpl::create_arg_expr_editor(const std::shared_ptr& arg) { + return std::make_shared(pane, arg); +} + +void NetEditorImpl::net_mutated(const std::shared_ptr&) { + pane->invalidate_wires(); +} + +// Arg editor callbacks — structural changes bubble up via node_mutated, so these are no-ops for now +void ArgNetEditorImpl::arg_net_mutated(const std::shared_ptr&) {} +void ArgNumberEditorImpl::arg_number_mutated(const std::shared_ptr&) {} +void ArgStringEditorImpl::arg_string_mutated(const std::shared_ptr&) {} +void ArgExprEditorImpl::arg_expr_mutated(const std::shared_ptr&) {} + +// ─── IGraphEditor implementation ─── + +std::shared_ptr Editor2Pane::node_added(const NodeId& id, const std::shared_ptr& node) { + auto ned = std::make_shared(this, node); + node_editors_[id] = ned; + wires_dirty_ = true; + return ned; +} + +void Editor2Pane::node_removed(const NodeId& id) { + node_editors_.erase(id); + wires_dirty_ = true; +} + +std::shared_ptr Editor2Pane::net_added(const NodeId& id, const std::shared_ptr& net) { + auto ned = std::make_shared(this, net); + net_editors_[id] = ned; + wires_dirty_ = true; + return ned; +} + +void Editor2Pane::net_removed(const NodeId& id) { + net_editors_.erase(id); + wires_dirty_ = true; +} + // ─── Load ─── bool Editor2Pane::load(const std::string& path) { @@ -236,71 +206,31 @@ bool Editor2Pane::load(const std::string& path) { gb_ = std::get>(result); file_path_ = path; - // Extract tab name from path auto slash = path.find_last_of("/\\"); tab_name_ = (slash != std::string::npos) ? path.substr(slash + 1) : path; + // Register as editor — triggers node_added/net_added for all existing entries + gb_->add_editor(shared_from_this()); + printf("Editor2: loaded %zu entries from %s\n", gb_->entries.size(), path.c_str()); return true; } -// ─── Draw ─── - -void Editor2Pane::draw() { - if (!gb_) { - ImGui::TextDisabled("No file loaded"); - return; - } - - ImVec2 canvas_p0 = ImGui::GetCursorScreenPos(); - ImVec2 canvas_sz = ImGui::GetContentRegionAvail(); - if (canvas_sz.x < 50.0f) canvas_sz.x = 50.0f; - if (canvas_sz.y < 50.0f) canvas_sz.y = 50.0f; - - ImGui::InvisibleButton("##canvas2", canvas_sz, - ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight); - bool canvas_hovered = ImGui::IsItemHovered(); - - ImDrawList* dl = ImGui::GetWindowDrawList(); - - // Background - dl->AddRectFilled(canvas_p0, v2add(canvas_p0, canvas_sz), S.col_bg); - - ImVec2 canvas_origin = v2add(canvas_p0, canvas_offset_); - - // Grid - float grid_step = S.grid_step * canvas_zoom_; - if (grid_step > 5.0f) { - for (float x = fmodf(canvas_offset_.x, grid_step); x < canvas_sz.x; x += grid_step) - dl->AddLine({canvas_p0.x + x, canvas_p0.y}, {canvas_p0.x + x, canvas_p0.y + canvas_sz.y}, S.col_grid); - for (float y = fmodf(canvas_offset_.y, grid_step); y < canvas_sz.y; y += grid_step) - dl->AddLine({canvas_p0.x, canvas_p0.y + y}, {canvas_p0.x + canvas_sz.x, canvas_p0.y + y}, S.col_grid); - } - - // Clip - dl->PushClipRect(canvas_p0, v2add(canvas_p0, canvas_sz), true); - - // Draw nodes (skip shadows) - for (auto& [id, entry] : gb_->entries) { - if (auto node = entry->as_node()) { - if (node->shadow) throw std::logic_error("Editor2Pane: shadow nodes must be folded before rendering (id: " + id + ")"); - draw_node(dl, node, canvas_origin); - } - } - - // Draw wires by iterating each node's inputs/outputs/remaps - std::vector drawn_wires; // collect for hover testing +// ─── Wire rebuilding ─── - for (auto& [dst_id, dst_entry] : gb_->entries) { - auto dst_node = dst_entry->as_node(); - if (!dst_node) continue; +void Editor2Pane::rebuild_wires(ImVec2 canvas_origin) { + cached_wires_.clear(); + for (auto& [dst_id, ned] : node_editors_) { + auto dst_node = ned->node; auto* dst_nt = find_node_type2(dst_node->type_id); if (!dst_nt) continue; - auto dst_layout = compute_node_layout(dst_node, canvas_origin, canvas_zoom_); - // Helper: draw wire from a source to destination pin at index dst_pin - // Source can be a NetBuilder (regular wire) or FlowNodeBuilder (lambda capture) + // Rebuild layout for this node at current zoom + ned->rebuild(canvas_origin, canvas_zoom_); + auto& dst_layout = ned->layout; + auto& dst_pm = ned->pin_mapping; + auto draw_wire_to_pin = [&](int dst_pin, const BuilderEntryPtr& entry, const NodeId& net_id) { if (!entry) return; @@ -315,7 +245,6 @@ void Editor2Pane::draw() { src_node = src_ptr ? src_ptr->as_node() : nullptr; if (!src_node) return; named = !net->auto_wire(); - // Search fixed outputs for (int k = 0; k < (int)src_node->outputs.size(); k++) { auto out_net = src_node->outputs[k]->as_net(); if (out_net && out_net->second() == entry) { @@ -323,7 +252,6 @@ void Editor2Pane::draw() { break; } } - // Search output va_args if (source_pin == 0) { int base = (int)src_node->outputs.size(); for (int k = 0; k < (int)src_node->outputs_va_args.size(); k++) { @@ -344,7 +272,6 @@ void Editor2Pane::draw() { auto* src_nt = find_node_type2(src_node->type_id); auto src_layout = compute_node_layout(src_node, canvas_origin, canvas_zoom_); ImVec2 to = dst_layout.input_pin_pos(dst_pin); - float th = S.wire_thickness * canvas_zoom_; ImVec2 from, cp1, cp2; ImU32 wire_col; @@ -365,7 +292,6 @@ void Editor2Pane::draw() { float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); cp1 = {from.x + dx, from.y}; cp2 = {to.x, to.y - dy}; } else { - // Visual pin index: skip side-bang for flow nodes int visual_pin = source_pin; if (src_nt && src_nt->is_flow()) visual_pin = std::max(0, visual_pin - 1); from = src_layout.output_pin_pos(visual_pin); @@ -375,28 +301,9 @@ void Editor2Pane::draw() { wire_col = named ? S.col_wire_named : S.col_wire; } - dl->AddBezierCubic(from, cp1, cp2, to, wire_col, th); - drawn_wires.push_back({ entry, from, cp1, cp2, to, src_node->id(), dst_id, net_id}); - - // Label for named nets - if (named) { - float font_size = ImGui::GetFontSize() * canvas_zoom_ * 0.8f; - if (font_size > 5.0f) { - ImVec2 mid = {(from.x + to.x) * 0.5f, (from.y + to.y) * 0.5f}; - ImVec2 text_sz = ImGui::CalcTextSize(net_id.c_str()); - float tw = text_sz.x * (font_size / ImGui::GetFontSize()); - float tth = text_sz.y * (font_size / ImGui::GetFontSize()); - float cx = mid.x - tw * 0.5f; - float cy = mid.y - tth * 0.5f; - dl->AddRectFilled({cx - 3, cy - 1}, {cx + tw + 3, cy + tth + 1}, - S.col_label_bg, S.node_rounding); - dl->AddText(nullptr, font_size, {cx, cy}, S.col_label_text, net_id.c_str()); - } - } + cached_wires_.push_back({entry, from, cp1, cp2, to, src_node->id(), dst_id, net_id}); }; - // Draw wires using PinMapping for correct pin positions - auto dst_pm = PinMapping::build(dst_node, dst_nt); for (int i = 0; i < dst_pm.total(); i++) { if (dst_pm.is_add_diamond(i)) continue; if (dst_pm.is_absent_optional(i)) continue; @@ -422,16 +329,96 @@ void Editor2Pane::draw() { } } + wires_dirty_ = false; +} + +// ─── Draw ─── + +void Editor2Pane::draw() { + if (!gb_) { + ImGui::TextDisabled("No file loaded"); + return; + } + + ImVec2 canvas_p0 = ImGui::GetCursorScreenPos(); + ImVec2 canvas_sz = ImGui::GetContentRegionAvail(); + if (canvas_sz.x < 50.0f) canvas_sz.x = 50.0f; + if (canvas_sz.y < 50.0f) canvas_sz.y = 50.0f; + + ImGui::InvisibleButton("##canvas2", canvas_sz, + ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight); + bool canvas_hovered = ImGui::IsItemHovered(); + + ImDrawList* dl = ImGui::GetWindowDrawList(); + + // Background + dl->AddRectFilled(canvas_p0, v2add(canvas_p0, canvas_sz), S.col_bg); + + ImVec2 canvas_origin = v2add(canvas_p0, canvas_offset_); + + // Grid + float grid_step = S.grid_step * canvas_zoom_; + if (grid_step > 5.0f) { + for (float x = fmodf(canvas_offset_.x, grid_step); x < canvas_sz.x; x += grid_step) + dl->AddLine({canvas_p0.x + x, canvas_p0.y}, {canvas_p0.x + x, canvas_p0.y + canvas_sz.y}, S.col_grid); + for (float y = fmodf(canvas_offset_.y, grid_step); y < canvas_sz.y; y += grid_step) + dl->AddLine({canvas_p0.x, canvas_p0.y + y}, {canvas_p0.x + canvas_sz.x, canvas_p0.y + y}, S.col_grid); + } + + // Clip + dl->PushClipRect(canvas_p0, v2add(canvas_p0, canvas_sz), true); + + // Rebuild node layouts and draw nodes + for (auto& [id, ned] : node_editors_) { + if (ned->node->shadow) + throw std::logic_error("Editor2Pane: shadow nodes must be folded before rendering (id: " + id + ")"); + ned->rebuild(canvas_origin, canvas_zoom_); + draw_node(dl, ned, canvas_origin); + } + + // Rebuild wires if dirty (always dirty after layout rebuild since positions depend on zoom/pan) + rebuild_wires(canvas_origin); + + // Draw wires + for (auto& w : cached_wires_) { + // Determine wire color + bool is_lambda = w.is_lambda(); + bool named = false; + if (!is_lambda) { + if (auto net = w.entry_->as_net()) + named = !net->auto_wire(); + } + ImU32 wire_col = is_lambda ? S.col_wire_lambda : (named ? S.col_wire_named : S.col_wire); + float th = S.wire_thickness * canvas_zoom_; + dl->AddBezierCubic(w.p0, w.p1, w.p2, w.p3, wire_col, th); + + // Label for named nets + if (named) { + float font_size = ImGui::GetFontSize() * canvas_zoom_ * 0.8f; + if (font_size > 5.0f) { + ImVec2 mid = {(w.p0.x + w.p3.x) * 0.5f, (w.p0.y + w.p3.y) * 0.5f}; + ImVec2 text_sz = ImGui::CalcTextSize(w.net_id.c_str()); + float tw = text_sz.x * (font_size / ImGui::GetFontSize()); + float tth = text_sz.y * (font_size / ImGui::GetFontSize()); + float cx = mid.x - tw * 0.5f; + float cy = mid.y - tth * 0.5f; + dl->AddRectFilled({cx - 3, cy - 1}, {cx + tw + 3, cy + tth + 1}, + S.col_label_bg, S.node_rounding); + dl->AddText(nullptr, font_size, {cx, cy}, S.col_label_text, w.net_id.c_str()); + } + } + } + dl->PopClipRect(); // ─── Hover detection + effects ─── if (canvas_hovered) { ImVec2 mouse = ImGui::GetIO().MousePos; - hover_item_ = detect_hover(mouse, canvas_origin, drawn_wires); + hover_item_ = detect_hover(mouse, canvas_origin); } else { hover_item_ = std::monostate{}; } - draw_hover_effects(dl, canvas_origin, drawn_wires, hover_item_); + draw_hover_effects(dl, canvas_origin, hover_item_); // Extract hover node from variant FlowNodeBuilderPtr hover_node = nullptr; @@ -446,27 +433,24 @@ void Editor2Pane::draw() { bool ctrl = ImGui::GetIO().KeyCtrl; if (ctrl && hover_node) { - // Ctrl+click: toggle node in selection if (selected_nodes_.count(hover_node)) selected_nodes_.erase(hover_node); else selected_nodes_.insert(hover_node); } else if (hover_node) { - // Regular click on node: select if not already, start drag if (!selected_nodes_.count(hover_node)) { selected_nodes_.clear(); selected_nodes_.insert(hover_node); } dragging_started_ = true; - // Check if any selected node is already overlapping at drag start drag_was_overlapping_ = false; float pad = S.node_height * 0.5f; for (auto& sel : selected_nodes_) { auto sel_layout = compute_node_layout(sel, {0,0}, 1.0f); - for (auto& [oid, oe] : gb_->entries) { - auto on = oe->as_node(); - if (!on || selected_nodes_.count(on)) continue; + for (auto& [oid, oned] : node_editors_) { + auto on = oned->node; + if (selected_nodes_.count(on)) continue; auto ol = compute_node_layout(on, {0,0}, 1.0f); if (sel->position.x < on->position.x - pad + ol.width + pad * 2 && sel->position.x + sel_layout.width > on->position.x - pad && @@ -479,7 +463,6 @@ void Editor2Pane::draw() { if (drag_was_overlapping_) break; } } else { - // Clicked on empty space or wire — start selection rectangle selected_nodes_.clear(); selection_rect_active_ = true; ImVec2 mouse = ImGui::GetIO().MousePos; @@ -494,16 +477,15 @@ void Editor2Pane::draw() { float dx = delta.x / canvas_zoom_; float dy = delta.y / canvas_zoom_; - // Check overlap for all selected nodes against all non-selected nodes bool blocked = false; if (!drag_was_overlapping_) { float pad = S.node_height * 0.5f; for (auto& sel : selected_nodes_) { auto sel_layout = compute_node_layout(sel, {0,0}, 1.0f); float nx = sel->position.x + dx, ny = sel->position.y + dy; - for (auto& [oid, oe] : gb_->entries) { - auto on = oe->as_node(); - if (!on || selected_nodes_.count(on)) continue; + for (auto& [oid, oned] : node_editors_) { + auto on = oned->node; + if (selected_nodes_.count(on)) continue; auto ol = compute_node_layout(on, {0,0}, 1.0f); float ox = on->position.x - pad, oy = on->position.y - pad; float ow = ol.width + pad * 2, oh = ol.height + pad * 2; @@ -521,10 +503,11 @@ void Editor2Pane::draw() { sel->position.x += dx; sel->position.y += dy; } + wires_dirty_ = true; } } - // Selection rectangle — continuously update selection each frame + // Selection rectangle if (selection_rect_active_) { ImVec2 mouse = ImGui::GetIO().MousePos; ImVec2 cur_canvas = {(mouse.x - canvas_origin.x) / canvas_zoom_, @@ -535,17 +518,14 @@ void Editor2Pane::draw() { float x1 = std::max(selection_rect_start_.x, cur_canvas.x); float y1 = std::max(selection_rect_start_.y, cur_canvas.y); - // Draw selection rect ImVec2 sp0 = {canvas_origin.x + x0 * canvas_zoom_, canvas_origin.y + y0 * canvas_zoom_}; ImVec2 sp1 = {canvas_origin.x + x1 * canvas_zoom_, canvas_origin.y + y1 * canvas_zoom_}; dl->AddRectFilled(sp0, sp1, IM_COL32(100, 130, 200, 40)); dl->AddRect(sp0, sp1, IM_COL32(100, 130, 200, 180), 0, 0, 1.5f); - // Recalculate selection set every frame selected_nodes_.clear(); - for (auto& [id, entry] : gb_->entries) { - auto node = entry->as_node(); - if (!node) continue; + for (auto& [id, ned] : node_editors_) { + auto node = ned->node; auto layout = compute_node_layout(node, {0,0}, 1.0f); float nx0 = node->position.x, ny0 = node->position.y; float nx1 = nx0 + layout.width, ny1 = ny0 + layout.height; @@ -576,7 +556,6 @@ void Editor2Pane::draw() { float old_zoom = canvas_zoom_; canvas_zoom_ *= (wheel > 0) ? 1.1f : 0.9f; canvas_zoom_ = std::clamp(canvas_zoom_, 0.1f, 10.0f); - // Zoom toward mouse position ImVec2 mouse = ImGui::GetIO().MousePos; ImVec2 mouse_rel = v2sub(v2sub(mouse, canvas_p0), canvas_offset_); ImVec2 mouse_canvas = v2mul(mouse_rel, 1.0f / old_zoom); @@ -587,16 +566,17 @@ void Editor2Pane::draw() { // ─── Draw a node ─── -void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, +void Editor2Pane::draw_node(ImDrawList* dl, const std::shared_ptr& ned, ImVec2 canvas_origin) { + auto& node = ned->node; auto* nt = find_node_type2(node->type_id); if (!nt) return; - auto layout = compute_node_layout(node, canvas_origin, canvas_zoom_); + auto& layout = ned->layout; + auto& pm = ned->pin_mapping; // Special nodes: label and error if (nt->is_special()) { - // Display first arg without quotes std::string display; if (node->parsed_args && !node->parsed_args->empty()) { auto a = (*node->parsed_args)[0]; @@ -609,13 +589,11 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, bool is_error = (node->type_id == NodeTypeID::Error); if (is_error) { - // Error: red box dl->AddRectFilled(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, S.col_node_err, S.node_rounding * canvas_zoom_); dl->AddRect(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, S.col_err_border, S.node_rounding * canvas_zoom_); } - // Label: no box at all if (font_size > 5.0f) { ImVec2 text_sz = ImGui::CalcTextSize(display.c_str()); @@ -624,27 +602,20 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, float cy = layout.pos.y + (layout.height - font_size) * 0.5f; dl->AddText(nullptr, font_size, {cx, cy}, S.col_text, display.c_str()); } - return; // no pins for special nodes + return; } - // Display text - std::string display = nt->name; - std::string args = node->args_str(); - if (!args.empty()) display += " " + args; - bool selected = selected_nodes_.count(node); - // node_hovered = hover_item_ is this node directly (not a pin on it) bool node_hovered = false; if (auto* ep = std::get_if(&hover_item_)) node_hovered = (*ep == node); - // pin_hovered_on_this_node = hover_item_ is a pin or +diamond belonging to this node bool pin_hovered_on_this = false; if (auto* pin = std::get_if(&hover_item_)) pin_hovered_on_this = ((*pin)->node() == node); else if (auto* add = std::get_if(&hover_item_)) pin_hovered_on_this = (add->node == node); - bool has_error = !node->error.empty(); + bool has_error = ned->has_error; ImU32 col = has_error ? S.col_node_err : (selected ? S.col_node_sel : S.col_node); dl->AddRectFilled(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, @@ -656,20 +627,18 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, // Text float font_size = ImGui::GetFontSize() * canvas_zoom_; if (font_size > 5.0f) { - ImVec2 text_sz = ImGui::CalcTextSize(display.c_str()); + ImVec2 text_sz = ImGui::CalcTextSize(ned->display_text.c_str()); float tw = text_sz.x * canvas_zoom_; float cx = layout.pos.x + (layout.width - tw) * 0.5f; float cy = layout.pos.y + (layout.height - font_size) * 0.5f; - dl->AddText(nullptr, font_size, {cx, cy}, S.col_text, display.c_str()); + dl->AddText(nullptr, font_size, {cx, cy}, S.col_text, ned->display_text.c_str()); } - // Draw input pins (top) using PinMapping for correct port-to-pin association + // Draw input pins (top) using PinMapping float pr = S.pin_radius * canvas_zoom_; - auto pm = PinMapping::build(node, nt); for (int i = 0; i < layout.num_in; i++) { ImVec2 pp = layout.input_pin_pos(i); - // +diamond slot if (pm.is_add_diamond(i)) { ImU32 pc = S.col_add_pin; dl->AddQuadFilled({pp.x, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x, pp.y + pr}, {pp.x - pr, pp.y}, pc); @@ -697,7 +666,6 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, } else if (is_input_va) { kind = nt->input_ports_va_args ? nt->input_ports_va_args->kind : PortKind2::Data; } - // remaps are always Data ImU32 pc = pin_color(kind); if (kind == PortKind2::BangTrigger) { @@ -721,13 +689,13 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, } } - // Draw output pins (bottom): skip side-bang for flow nodes, then fixed outputs, then va_args + // Draw output pins (bottom) int skip_sb = (nt->is_flow()) ? 1 : 0; int fixed_out = nt->num_outputs; - int rendered_fixed = fixed_out - skip_sb; // fixed outputs rendered at bottom + int rendered_fixed = fixed_out - skip_sb; for (int i = 0; i < layout.num_out; i++) { ImVec2 pp = layout.output_pin_pos(i); - int out_idx = i + skip_sb; // index into outputs[] (skip side-bang) + int out_idx = i + skip_sb; bool is_output_va = (i >= rendered_fixed); PortKind2 kind = PortKind2::Data; @@ -748,7 +716,6 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, // Flow-only: lambda grab (left) and side-bang (right) if (nt->is_flow()) { - // Lambda grab handle (left-pointing triangle, middle-left) ImVec2 gp = layout.lambda_grab_pos(); ImU32 lc = S.col_pin_lambda; dl->AddTriangleFilled( @@ -756,7 +723,6 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, {gp.x - pr, gp.y}, {gp.x + pr, gp.y + pr}, lc); - // Outline when node is hovered (not pin) if (node_hovered) { float ho = S.highlight_offset * canvas_zoom_; dl->AddTriangle( @@ -766,12 +732,11 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, S.col_pin_hover, S.highlight_thickness); } - // Side-bang (square, middle-right) ImVec2 bp = {layout.pos.x + layout.width, layout.pos.y + layout.height * 0.5f}; dl->AddRectFilled({bp.x - pr, bp.y - pr}, {bp.x + pr, bp.y + pr}, S.col_pin_bang); } - // Pin/node hover visuals driven by hover_item_ + // Pin/node hover visuals if (!node_hovered && !pin_hovered_on_this) return; float ho = S.highlight_offset * canvas_zoom_; @@ -789,15 +754,13 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, } }; - // Get the hovered pin (if any) FlowArg2Ptr hovered_pin = nullptr; if (auto* pp = std::get_if(&hover_item_)) hovered_pin = *pp; - // Check if +diamond is hovered + // +diamond hover if (auto* add_hover = std::get_if(&hover_item_)) { if (add_hover->node == node) { - // Find the +diamond position and highlight it for (int i = 0; i < pm.total(); i++) { if (pm.is_add_diamond(i)) { ImVec2 pp = layout.input_pin_pos(i); @@ -815,7 +778,6 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, } if (hovered_pin) { - // Find which visual pin matches and highlight it // Input pins for (int i = 0; i < pm.total(); i++) { if (pm.is_add_diamond(i) || pm.is_absent_optional(i)) continue; @@ -857,7 +819,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, return; } } - // Output pins (skip side-bang, then fixed, then va_args) + // Output pins { int hl_skip_sb = nt->is_flow() ? 1 : 0; int hl_rendered_fixed = fixed_out - hl_skip_sb; @@ -906,7 +868,7 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, } } - // Node body tooltip (when node is hovered directly, not a pin) + // Node body tooltip if (node_hovered && draw_tooltips_) { ImGui::BeginTooltip(); ImGui::SetWindowFontScale(S.tooltip_scale); @@ -941,21 +903,17 @@ void Editor2Pane::draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, } void Editor2Pane::draw_net(ImDrawList*, const NetBuilderPtr&, ImVec2) { - // Unused — wires drawn per-node in draw() + // Unused — wires drawn via cached_wires_ in draw() } // ─── Hover detection ─── -Editor2Pane::HoverItem Editor2Pane::detect_hover( - ImVec2 mouse, ImVec2 canvas_origin, const std::vector& drawn_wires) -{ - // Smallest distance wins across wires, nodes, and pins +Editor2Pane::HoverItem Editor2Pane::detect_hover(ImVec2 mouse, ImVec2 canvas_origin) { auto d2 = [](ImVec2 a, ImVec2 b) { return std::sqrt((a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y)); }; float best_dist = 1e18f; HoverItem result = std::monostate{}; - // Pins get a large bias so they always win over nodes/wires when within threshold float pin_bias = S.pin_priority_bias; auto try_candidate = [&](float dist, HoverItem candidate) { @@ -967,24 +925,21 @@ Editor2Pane::HoverItem Editor2Pane::detect_hover( // Wires float wire_thresh = S.wire_hit_threshold * canvas_zoom_; - for (auto& w : drawn_wires) { + for (auto& w : cached_wires_) { float d = point_to_bezier_dist(mouse, w.p0, w.p1, w.p2, w.p3); if (d < wire_thresh) try_candidate(d, w.entry()); } - // Nodes — distance from mouse to nearest edge of node rect - for (auto it = gb_->entries.rbegin(); it != gb_->entries.rend(); ++it) { - auto node = it->second->as_node(); - if (!node) continue; - auto layout = compute_node_layout(node, canvas_origin, canvas_zoom_); + // Nodes + for (auto it = node_editors_.rbegin(); it != node_editors_.rend(); ++it) { + auto& ned = it->second; + auto& layout = ned->layout; - // Distance from mouse to nearest point on node outline float nd; bool inside = mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height; if (inside) { - // Inside: distance to nearest edge float dl_ = mouse.x - layout.pos.x; float dr = layout.pos.x + layout.width - mouse.x; float dt = mouse.y - layout.pos.y; @@ -996,20 +951,20 @@ Editor2Pane::HoverItem Editor2Pane::detect_hover( nd = d2(mouse, {cx, cy}); } if (nd < S.pin_radius * canvas_zoom_ * S.node_hit_threshold_mul) - try_candidate(nd, BuilderEntryPtr(node)); + try_candidate(nd, BuilderEntryPtr(ned->node)); } - // Pins — check all nodes, find closest pin globally - for (auto it = gb_->entries.rbegin(); it != gb_->entries.rend(); ++it) { - auto node = it->second->as_node(); - if (!node) continue; + // Pins + for (auto it = node_editors_.rbegin(); it != node_editors_.rend(); ++it) { + auto& ned = it->second; + auto& node = ned->node; auto* nt = find_node_type2(node->type_id); if (!nt) continue; - auto layout = compute_node_layout(node, canvas_origin, canvas_zoom_); + auto& layout = ned->layout; + auto& pm = ned->pin_mapping; float pin_thresh = S.pin_radius * canvas_zoom_ * S.pin_hit_radius_mul; - auto pm = PinMapping::build(node, nt); - // Input pins (including +diamond) + // Input pins for (int i = 0; i < pm.total(); i++) { if (pm.is_absent_optional(i)) continue; float pd = d2(mouse, layout.input_pin_pos(i)); @@ -1037,7 +992,7 @@ Editor2Pane::HoverItem Editor2Pane::detect_hover( } } - // Output pins (skip side-bang for flow, then fixed, then va_args) + // Output pins { int skip_sb = nt->is_flow() ? 1 : 0; int rendered_fixed = nt->num_outputs - skip_sb; @@ -1057,14 +1012,14 @@ Editor2Pane::HoverItem Editor2Pane::detect_hover( } } - // Lambda grab → node itself (pin-level priority) + // Lambda grab if (nt->is_flow()) { float pd = d2(mouse, layout.lambda_grab_pos()); if (pd < pin_thresh) try_candidate(pd - pin_bias, BuilderEntryPtr(node)); } - // Side-bang → first output (pin-level priority) + // Side-bang if (nt->is_flow()) { ImVec2 bp = {layout.pos.x + layout.width, layout.pos.y + layout.height * 0.5f}; float pd = d2(mouse, bp); @@ -1082,14 +1037,12 @@ Editor2Pane::HoverItem Editor2Pane::detect_hover( // ─── Hover effects + tooltips ─── void Editor2Pane::draw_hover_effects( - ImDrawList* dl, ImVec2 canvas_origin, - const std::vector& drawn_wires, const HoverItem& hover) + ImDrawList* dl, ImVec2 canvas_origin, const HoverItem& hover) { if (std::holds_alternative(hover)) return; float th_wire = (S.wire_thickness + 2.0f) * canvas_zoom_; - // Determine what's hovered FlowNodeBuilderPtr hover_node = nullptr; BuilderEntryPtr hover_entry = nullptr; FlowArg2Ptr hover_pin = nullptr; @@ -1103,21 +1056,20 @@ void Editor2Pane::draw_hover_effects( // Node hovered: highlight lambda wires capturing it if (hover_node) { - for (auto& w : drawn_wires) { + for (auto& w : cached_wires_) { if (w.is_lambda() && w.entry() == hover_node) dl->AddBezierCubic(w.p0, w.p1, w.p2, w.p3, S.col_pin_hover, th_wire); } } - // Wire/net hovered: highlight all wires in the same net + lambda source node + // Wire/net hovered: highlight all wires in the same net if (hover_entry && hover_entry->as_net()) { - for (auto& w : drawn_wires) { + for (auto& w : cached_wires_) { if (w.entry() == hover_entry) dl->AddBezierCubic(w.p0, w.p1, w.p2, w.p3, S.col_pin_hover, th_wire); } if (draw_tooltips_) { - // Find the first wire for tooltip info - for (auto& w : drawn_wires) { + for (auto& w : cached_wires_) { if (w.entry() == hover_entry) { ImGui::BeginTooltip(); ImGui::SetWindowFontScale(S.tooltip_scale); diff --git a/src/attoflow/editor2.h b/src/attoflow/editor2.h index 30981ba..f6059c9 100644 --- a/src/attoflow/editor2.h +++ b/src/attoflow/editor2.h @@ -1,14 +1,177 @@ #pragma once #include "atto/graph_builder.h" +#include "atto/graph_editor_interfaces.h" #include "atto/node_types2.h" #include "imgui.h" #include #include #include +#include -// Editor2Pane: new editor using GraphBuilder exclusively. -// No FlowGraph, no inference, no codegen. -class Editor2Pane { +// ─── Style (extern for use in layout computation) ─── + +struct Editor2Style { + float node_min_width = 80.0f; + float node_height = 40.0f; + float pin_radius = 5.0f; + float pin_spacing = 16.0f; + float node_rounding = 4.0f; + float grid_step = 20.0f; + + float wire_thickness = 2.5f; + float node_border = 1.0f; + float highlight_offset = 2.0f; + float highlight_thickness = 2.0f; + float add_pin_line = 1.5f; + + float pin_hit_radius_mul = 2.5f; + float wire_hit_threshold = 30.0f; + float node_hit_threshold_mul = 6.f; + float dismiss_radius = 20.0f; + float pin_priority_bias = 1e6f; + + ImU32 col_bg = IM_COL32(30, 30, 40, 255); + ImU32 col_grid = IM_COL32(50, 50, 60, 255); + + ImU32 col_node = IM_COL32(50, 55, 75, 220); + ImU32 col_node_sel = IM_COL32(80, 90, 130, 255); + ImU32 col_node_err = IM_COL32(130, 40, 40, 220); + ImU32 col_node_border = IM_COL32(80, 80, 100, 255); + ImU32 col_err_border = IM_COL32(255, 80, 80, 255); + ImU32 col_text = IM_COL32(220, 220, 220, 255); + + ImU32 col_pin_data = IM_COL32(100, 200, 100, 255); + ImU32 col_pin_bang = IM_COL32(255, 200, 80, 255); + ImU32 col_pin_lambda = IM_COL32(180, 130, 255, 255); + ImU32 col_pin_hover = IM_COL32(255, 255, 255, 255); + ImU32 col_add_pin = IM_COL32(120, 120, 140, 180); + ImU32 col_add_pin_fg = IM_COL32(200, 200, 220, 220); + ImU32 col_opt_pin_fg = IM_COL32(30, 30, 40, 255); + + ImU32 col_wire = IM_COL32(200, 200, 100, 200); + ImU32 col_wire_named = IM_COL32(200, 200, 100, 120); + ImU32 col_wire_lambda = IM_COL32(180, 130, 255, 200); + + ImU32 col_label_bg = IM_COL32(30, 30, 40, 200); + ImU32 col_label_text = IM_COL32(180, 220, 255, 255); + + float tooltip_scale = 1.0f; +}; + +extern Editor2Style S; + +// ─── PinMapping: maps visible pin index → port descriptor ─── + +struct PinMapping { + std::vector pin_to_port; + int base_count = 0; + int va_count = 0; + int add_pin_pos = -1; + bool has_input_va = false; + + static PinMapping build(const FlowNodeBuilderPtr& node, const NodeType2* nt); + + int total() const { return (int)pin_to_port.size(); } + bool is_base(int pin) const { return pin < base_count; } + bool is_absent_optional(int pin) const { return pin < base_count && pin_to_port[pin] <= -3000; } + int absent_port_index(int pin) const { return -(pin_to_port[pin] + 3000); } + bool is_input_va(int pin) const { return pin >= base_count && pin < base_count + va_count; } + bool is_add_diamond(int pin) const { return pin == add_pin_pos; } + bool is_remap(int pin) const { return pin >= base_count + va_count + (has_input_va ? 1 : 0); } + int port_index(int pin) const { return pin_to_port[pin]; } + int remap_index(int pin) const { return -(pin_to_port[pin] + 2000); } +}; + +// ─── NodeLayout: computed screen-space layout for a node ─── + +struct NodeLayout { + ImVec2 pos; + float width; + float height; + int num_in; + int num_out; + float zoom; + + ImVec2 input_pin_pos(int i) const { + return {pos.x + (i + 0.5f) * S.pin_spacing * zoom, pos.y}; + } + ImVec2 output_pin_pos(int i) const { + return {pos.x + (i + 0.5f) * S.pin_spacing * zoom, pos.y + height}; + } + ImVec2 lambda_grab_pos() const { + return {pos.x, pos.y + height * 0.5f}; + } +}; + +NodeLayout compute_node_layout(const FlowNodeBuilderPtr& node, ImVec2 canvas_origin, float zoom); + +// ─── Forward declaration ─── + +class Editor2Pane; + +// ─── Per-item editor implementations ─── + +struct NodeEditorImpl : INodeEditor, std::enable_shared_from_this { + Editor2Pane* pane; + FlowNodeBuilderPtr node; + NodeLayout layout; + PinMapping pin_mapping; + std::string display_text; + bool has_error = false; + + NodeEditorImpl(Editor2Pane* p, const FlowNodeBuilderPtr& n) : pane(p), node(n) {} + + void rebuild(ImVec2 canvas_origin, float zoom); + + // INodeEditor + void node_mutated(const std::shared_ptr& node) override; + void node_layout_changed(const std::shared_ptr& node) override; + std::shared_ptr create_arg_net_editor(const std::shared_ptr& arg) override; + std::shared_ptr create_arg_number_editor(const std::shared_ptr& arg) override; + std::shared_ptr create_arg_string_editor(const std::shared_ptr& arg) override; + std::shared_ptr create_arg_expr_editor(const std::shared_ptr& arg) override; +}; + +struct NetEditorImpl : INetEditor { + Editor2Pane* pane; + NetBuilderPtr net; + + NetEditorImpl(Editor2Pane* p, const NetBuilderPtr& n) : pane(p), net(n) {} + + void net_mutated(const std::shared_ptr& net) override; +}; + +struct ArgNetEditorImpl : IArgNetEditor { + Editor2Pane* pane; + std::shared_ptr arg; + ArgNetEditorImpl(Editor2Pane* p, const std::shared_ptr& a) : pane(p), arg(a) {} + void arg_net_mutated(const std::shared_ptr& arg) override; +}; + +struct ArgNumberEditorImpl : IArgNumberEditor { + Editor2Pane* pane; + std::shared_ptr arg; + ArgNumberEditorImpl(Editor2Pane* p, const std::shared_ptr& a) : pane(p), arg(a) {} + void arg_number_mutated(const std::shared_ptr& arg) override; +}; + +struct ArgStringEditorImpl : IArgStringEditor { + Editor2Pane* pane; + std::shared_ptr arg; + ArgStringEditorImpl(Editor2Pane* p, const std::shared_ptr& a) : pane(p), arg(a) {} + void arg_string_mutated(const std::shared_ptr& arg) override; +}; + +struct ArgExprEditorImpl : IArgExprEditor { + Editor2Pane* pane; + std::shared_ptr arg; + ArgExprEditorImpl(Editor2Pane* p, const std::shared_ptr& a) : pane(p), arg(a) {} + void arg_expr_mutated(const std::shared_ptr& arg) override; +}; + +// ─── Editor2Pane ─── + +class Editor2Pane : public IGraphEditor, public std::enable_shared_from_this { public: // Load an .atto file (instrument@atto:0 format) bool load(const std::string& path); @@ -21,6 +184,15 @@ class Editor2Pane { const std::string& file_path() const { return file_path_; } const std::string& tab_name() const { return tab_name_; } + // IGraphEditor + std::shared_ptr node_added(const NodeId& id, const std::shared_ptr& node) override; + void node_removed(const NodeId& id) override; + std::shared_ptr net_added(const NodeId& id, const std::shared_ptr& net) override; + void net_removed(const NodeId& id) override; + + // Mark wires for rebuild (called by editor impls) + void invalidate_wires() { wires_dirty_ = true; } + private: std::shared_ptr gb_; std::string file_path_; @@ -33,19 +205,23 @@ class Editor2Pane { // Interaction state struct AddPinHover { FlowNodeBuilderPtr node; - const PortDesc2* va_port; // the va_args port descriptor template - bool is_input; // true = input +diamond, false = output +diamond (future) + const PortDesc2* va_port; + bool is_input; }; using HoverItem = std::variant; - HoverItem hover_item_; // monostate=nothing, BuilderEntryPtr=node/net, FlowArg2Ptr=pin, AddPinHover=+diamond + HoverItem hover_item_; bool draw_tooltips_ = true; std::set selected_nodes_; bool dragging_started_ = false; bool drag_was_overlapping_ = false; - bool selection_rect_active_ = false; // true when dragging a selection rectangle - ImVec2 selection_rect_start_ = {0, 0}; // canvas-space start point + bool selection_rect_active_ = false; + ImVec2 selection_rect_start_ = {0, 0}; int editing_link_id_ = -1; + // Per-item editor caches + std::map> node_editors_; + std::map> net_editors_; + // Wire info for hover hit-testing struct WireInfo { BuilderEntryPtr entry_; @@ -55,17 +231,20 @@ class Editor2Pane { bool is_lambda() const { return entry_ && entry_->is(IdCategory::Node); } }; + std::vector cached_wires_; + bool wires_dirty_ = true; + + void rebuild_wires(ImVec2 canvas_origin); + // Hover detection — returns best hover match - HoverItem detect_hover(ImVec2 mouse, ImVec2 canvas_origin, - const std::vector& drawn_wires); + HoverItem detect_hover(ImVec2 mouse, ImVec2 canvas_origin); // Tooltip + highlight drawing (driven by hover_item parameter) void draw_hover_effects(ImDrawList* dl, ImVec2 canvas_origin, - const std::vector& drawn_wires, const HoverItem& hover); // Drawing helpers - void draw_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, + void draw_node(ImDrawList* dl, const std::shared_ptr& ned, ImVec2 canvas_origin); void draw_net(ImDrawList* dl, const NetBuilderPtr& net, ImVec2 canvas_origin); From 243dc4fdc2052f9ae0a5a19e2a0541d3abadfc9f Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 22:12:46 +0200 Subject: [PATCH 75/86] Refactor node rendering and editor structure - Removed legacy style definitions and integrated them into a new `node_renderer` module. - Introduced `VisualPinMap` to manage visual representation of pins, replacing the previous `PinMapping`. - Updated `NodeLayout` to include methods for calculating pin positions and added support for side-bang positioning. - Implemented rendering functions for nodes, wires, and backgrounds in `node_renderer.cpp`. - Enhanced hit-testing functionality for nodes and pins, improving interaction responsiveness. - Cleaned up the `Editor2Pane` class by removing unused structures and methods related to pin handling. - Added tooltip support for node and pin interactions to enhance user experience. --- CMakeLists.txt | 1 + src/attoflow/editor2.cpp | 776 ++++----------------------------- src/attoflow/editor2.h | 137 +----- src/attoflow/node_renderer.cpp | 588 +++++++++++++++++++++++++ src/attoflow/node_renderer.h | 188 ++++++++ 5 files changed, 869 insertions(+), 821 deletions(-) create mode 100644 src/attoflow/node_renderer.cpp create mode 100644 src/attoflow/node_renderer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 626f89e..51a09f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,7 @@ if(ATTOLANG_BUILD_EDITOR) src/attoflow/main.cpp src/attoflow/editor.cpp src/attoflow/editor2.cpp + src/attoflow/node_renderer.cpp ) target_include_directories(attoflow PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 1c3cb58..9aba831 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -1,130 +1,20 @@ #include "editor2.h" +#include "node_renderer.h" #include "atto/graph_builder.h" #include "atto/node_types2.h" #include "imgui.h" #include #include #include -#include #include #include -// ─── Style (global instance) ─── - -Editor2Style S; - -// ─── Helpers ─── - -static float point_to_bezier_dist(ImVec2 p, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3) { - float min_d2 = 1e18f; - for (int i = 0; i <= 20; i++) { - float t = i / 20.0f; - float u = 1.0f - t; - float x = u*u*u*p0.x + 3*u*u*t*p1.x + 3*u*t*t*p2.x + t*t*t*p3.x; - float y = u*u*u*p0.y + 3*u*u*t*p1.y + 3*u*t*t*p2.y + t*t*t*p3.y; - float dx = p.x - x, dy = p.y - y; - float d2 = dx*dx + dy*dy; - if (d2 < min_d2) min_d2 = d2; - } - return std::sqrt(min_d2); -} - -static inline ImVec2 v2add(ImVec2 a, ImVec2 b) { return {a.x + b.x, a.y + b.y}; } -static inline ImVec2 v2sub(ImVec2 a, ImVec2 b) { return {a.x - b.x, a.y - b.y}; } -static inline ImVec2 v2mul(ImVec2 a, float s) { return {a.x * s, a.y * s}; } - -// ─── PinMapping::build ─── - -PinMapping PinMapping::build(const FlowNodeBuilderPtr& node, const NodeType2* nt) { - PinMapping m; - m.has_input_va = nt && nt->input_ports_va_args != nullptr; - int parsed_size = node->parsed_args ? (int)node->parsed_args->size() : 0; - - if (node->parsed_args) { - for (int i = 0; i < parsed_size; i++) { - if ((*node->parsed_args)[i]->is(ArgKind::Net)) { - m.pin_to_port.push_back(i); - m.base_count++; - } - } - } - if (nt) { - for (int i = parsed_size; i < nt->total_inputs(); i++) { - if (i >= nt->num_inputs) { - m.pin_to_port.push_back(-3000 - i); - m.base_count++; - } - } - } - if (node->parsed_va_args) { - for (int i = 0; i < (int)node->parsed_va_args->size(); i++) { - if ((*node->parsed_va_args)[i]->is(ArgKind::Net)) { - m.pin_to_port.push_back(-(i + 1)); - m.va_count++; - } - } - } - if (m.has_input_va) { - m.add_pin_pos = (int)m.pin_to_port.size(); - m.pin_to_port.push_back(-1000); - } - for (int i = 0; i < (int)node->remaps.size(); i++) { - m.pin_to_port.push_back(-2000 - i); - } - return m; -} - -// ─── compute_node_layout ─── - -NodeLayout compute_node_layout(const FlowNodeBuilderPtr& node, ImVec2 canvas_origin, float zoom) { - auto* nt = find_node_type2(node->type_id); - std::string display = nt ? nt->name : "?"; - std::string args = node->args_str(); - if (!args.empty()) display += " " + args; - - float font_size = ImGui::GetFontSize() * zoom; - ImVec2 text_sz = ImGui::CalcTextSize(display.c_str()); - float text_w = text_sz.x * zoom + 16.0f * zoom; - - auto pm = PinMapping::build(node, nt); - int num_in = pm.total(); - - int fixed_out = nt ? nt->num_outputs : (int)node->outputs.size(); - int skip_side_bang = 0; - if (nt && nt->is_flow()) { - if (node->outputs.empty()) - throw std::logic_error("Flow node '" + node->id() + "' must have at least one output (side-bang)"); - skip_side_bang = 1; - } - int va_out = (int)node->outputs_va_args.size(); - int num_out = std::max(0, fixed_out - skip_side_bang) + va_out; - - float pin_w_top = std::max(0, num_in) * S.pin_spacing * zoom; - float pin_w_bot = std::max(0, num_out) * S.pin_spacing * zoom; - float node_w = std::max({S.node_min_width * zoom, text_w, pin_w_top, pin_w_bot}); - float node_h = S.node_height * zoom; - - ImVec2 pos = {canvas_origin.x + node->position.x * zoom, - canvas_origin.y + node->position.y * zoom}; - - return {pos, node_w, node_h, num_in, num_out, zoom}; -} - -static ImU32 pin_color(PortKind2 kind) { - switch (kind) { - case PortKind2::BangTrigger: - case PortKind2::BangNext: return S.col_pin_bang; - case PortKind2::Lambda: return S.col_pin_lambda; - default: return S.col_pin_data; - } -} - // ─── Per-item editor implementations ─── void NodeEditorImpl::rebuild(ImVec2 canvas_origin, float zoom) { auto* nt = find_node_type2(node->type_id); - layout = compute_node_layout(node, canvas_origin, zoom); - pin_mapping = PinMapping::build(node, nt); + vpm = VisualPinMap::build(node, nt); + layout = compute_node_layout(node, vpm, canvas_origin, zoom); display_text = nt ? nt->name : "?"; std::string args = node->args_str(); if (!args.empty()) display_text += " " + args; @@ -156,7 +46,6 @@ void NetEditorImpl::net_mutated(const std::shared_ptr&) { pane->invalidate_wires(); } -// Arg editor callbacks — structural changes bubble up via node_mutated, so these are no-ops for now void ArgNetEditorImpl::arg_net_mutated(const std::shared_ptr&) {} void ArgNumberEditorImpl::arg_number_mutated(const std::shared_ptr&) {} void ArgStringEditorImpl::arg_string_mutated(const std::shared_ptr&) {} @@ -209,7 +98,6 @@ bool Editor2Pane::load(const std::string& path) { auto slash = path.find_last_of("/\\"); tab_name_ = (slash != std::string::npos) ? path.substr(slash + 1) : path; - // Register as editor — triggers node_added/net_added for all existing entries gb_->add_editor(shared_from_this()); printf("Editor2: loaded %zu entries from %s\n", gb_->entries.size(), path.c_str()); @@ -226,25 +114,31 @@ void Editor2Pane::rebuild_wires(ImVec2 canvas_origin) { auto* dst_nt = find_node_type2(dst_node->type_id); if (!dst_nt) continue; - // Rebuild layout for this node at current zoom ned->rebuild(canvas_origin, canvas_zoom_); auto& dst_layout = ned->layout; - auto& dst_pm = ned->pin_mapping; + auto& dst_vpm = ned->vpm; + + // For each input pin with a net connection, compute wire geometry + for (int i = 0; i < (int)dst_vpm.inputs.size(); i++) { + auto& pin = dst_vpm.inputs[i]; + if (pin.kind == VisualPinKind::AddDiamond || pin.kind == VisualPinKind::AbsentOptional) continue; + if (!pin.arg) continue; + auto an = pin.arg->as_net(); + if (!an) continue; - auto draw_wire_to_pin = [&](int dst_pin, const BuilderEntryPtr& entry, const NodeId& net_id) { - if (!entry) return; + auto entry = an->second(); + if (!entry) continue; FlowNodeBuilderPtr src_node = nullptr; - bool named = false; bool is_lambda = false; int source_pin = 0; if (auto net = entry->as_net()) { - if (net->is_the_unconnected()) return; + if (net->is_the_unconnected()) continue; auto src_ptr = net->source().lock(); src_node = src_ptr ? src_ptr->as_node() : nullptr; - if (!src_node) return; - named = !net->auto_wire(); + if (!src_node) continue; + // Find which output pin sources this net for (int k = 0; k < (int)src_node->outputs.size(); k++) { auto out_net = src_node->outputs[k]->as_net(); if (out_net && out_net->second() == entry) { @@ -266,66 +160,36 @@ void Editor2Pane::rebuild_wires(ImVec2 canvas_origin) { src_node = node; is_lambda = true; } else { - return; + continue; } auto* src_nt = find_node_type2(src_node->type_id); - auto src_layout = compute_node_layout(src_node, canvas_origin, canvas_zoom_); - ImVec2 to = dst_layout.input_pin_pos(dst_pin); + auto src_vpm = VisualPinMap::build(src_node, src_nt); + auto src_layout = compute_node_layout(src_node, src_vpm, canvas_origin, canvas_zoom_); + ImVec2 to = dst_layout.input_pin_pos(i); - ImVec2 from, cp1, cp2; - ImU32 wire_col; + ImVec2 from; + bool is_side_bang = false; if (is_lambda) { from = src_layout.lambda_grab_pos(); - float dx = std::max(std::abs(to.x - from.x) * 0.5f, 30.0f * canvas_zoom_); - float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); - cp1 = {from.x - dx, from.y}; cp2 = {to.x, to.y - dy}; - wire_col = S.col_wire_lambda; } else { - bool is_side_bang = src_nt && src_nt->is_flow() && + is_side_bang = src_nt && src_nt->is_flow() && source_pin < (src_nt->num_outputs) && src_nt->output_ports && src_nt->output_ports[source_pin].kind == PortKind2::BangNext; if (is_side_bang) { - from = {src_layout.pos.x + src_layout.width, src_layout.pos.y + src_layout.height * 0.5f}; - float dx = std::max(std::abs(to.x - from.x) * 0.5f, 30.0f * canvas_zoom_); - float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); - cp1 = {from.x + dx, from.y}; cp2 = {to.x, to.y - dy}; + from = src_layout.side_bang_pos(); } else { + // Map source_pin to visual output index (skip side-bang for flow) int visual_pin = source_pin; if (src_nt && src_nt->is_flow()) visual_pin = std::max(0, visual_pin - 1); from = src_layout.output_pin_pos(visual_pin); - float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * canvas_zoom_); - cp1 = {from.x, from.y + dy}; cp2 = {to.x, to.y - dy}; } - wire_col = named ? S.col_wire_named : S.col_wire; } - cached_wires_.push_back({entry, from, cp1, cp2, to, src_node->id(), dst_id, net_id}); - }; - - for (int i = 0; i < dst_pm.total(); i++) { - if (dst_pm.is_add_diamond(i)) continue; - if (dst_pm.is_absent_optional(i)) continue; - if (dst_pm.is_base(i)) { - int port = dst_pm.port_index(i); - if (dst_node->parsed_args && port < (int)dst_node->parsed_args->size()) { - if (auto an = (*dst_node->parsed_args)[port]->as_net()) - draw_wire_to_pin(i, an->second(), an->first()); - } - } else if (dst_pm.is_input_va(i)) { - int va_idx = -(dst_pm.port_index(i) + 1); - if (dst_node->parsed_va_args && va_idx < (int)dst_node->parsed_va_args->size()) { - if (auto an = (*dst_node->parsed_va_args)[va_idx]->as_net()) - draw_wire_to_pin(i, an->second(), an->first()); - } - } else if (dst_pm.is_remap(i)) { - int ri = dst_pm.remap_index(i); - if (ri < (int)dst_node->remaps.size()) { - if (auto an = dst_node->remaps[ri]->as_net()) - draw_wire_to_pin(i, an->second(), an->first()); - } - } + cached_wires_.push_back(compute_wire_geometry( + from, to, is_lambda, is_side_bang, canvas_zoom_, + entry, src_node->id(), dst_id, an->first())); } } @@ -350,22 +214,10 @@ void Editor2Pane::draw() { bool canvas_hovered = ImGui::IsItemHovered(); ImDrawList* dl = ImGui::GetWindowDrawList(); - - // Background - dl->AddRectFilled(canvas_p0, v2add(canvas_p0, canvas_sz), S.col_bg); - ImVec2 canvas_origin = v2add(canvas_p0, canvas_offset_); - // Grid - float grid_step = S.grid_step * canvas_zoom_; - if (grid_step > 5.0f) { - for (float x = fmodf(canvas_offset_.x, grid_step); x < canvas_sz.x; x += grid_step) - dl->AddLine({canvas_p0.x + x, canvas_p0.y}, {canvas_p0.x + x, canvas_p0.y + canvas_sz.y}, S.col_grid); - for (float y = fmodf(canvas_offset_.y, grid_step); y < canvas_sz.y; y += grid_step) - dl->AddLine({canvas_p0.x, canvas_p0.y + y}, {canvas_p0.x + canvas_sz.x, canvas_p0.y + y}, S.col_grid); - } + render_background(dl, canvas_p0, canvas_sz, canvas_offset_, canvas_zoom_); - // Clip dl->PushClipRect(canvas_p0, v2add(canvas_p0, canvas_sz), true); // Rebuild node layouts and draw nodes @@ -373,40 +225,36 @@ void Editor2Pane::draw() { if (ned->node->shadow) throw std::logic_error("Editor2Pane: shadow nodes must be folded before rendering (id: " + id + ")"); ned->rebuild(canvas_origin, canvas_zoom_); - draw_node(dl, ned, canvas_origin); + + // Build render state from editor interaction state + auto& node = ned->node; + NodeRenderState state; + state.selected = selected_nodes_.count(node) > 0; + state.node_hovered = false; + if (auto* ep = std::get_if(&hover_item_)) + state.node_hovered = (*ep == node); + state.pin_hovered_on_this = false; + if (auto* pin = std::get_if(&hover_item_)) + state.pin_hovered_on_this = ((*pin)->node() == node); + else if (auto* add = std::get_if(&hover_item_)) + state.pin_hovered_on_this = (add->node == node); + state.hovered_pin = nullptr; + if (auto* pp = std::get_if(&hover_item_)) + state.hovered_pin = *pp; + state.add_pin_hover = std::get_if(&hover_item_); + + auto* nt = find_node_type2(node->type_id); + render_node(dl, node, nt, ned->layout, ned->vpm, ned->display_text, + state, canvas_zoom_, draw_tooltips_); } - // Rebuild wires if dirty (always dirty after layout rebuild since positions depend on zoom/pan) + // Rebuild wires rebuild_wires(canvas_origin); // Draw wires for (auto& w : cached_wires_) { - // Determine wire color - bool is_lambda = w.is_lambda(); - bool named = false; - if (!is_lambda) { - if (auto net = w.entry_->as_net()) - named = !net->auto_wire(); - } - ImU32 wire_col = is_lambda ? S.col_wire_lambda : (named ? S.col_wire_named : S.col_wire); - float th = S.wire_thickness * canvas_zoom_; - dl->AddBezierCubic(w.p0, w.p1, w.p2, w.p3, wire_col, th); - - // Label for named nets - if (named) { - float font_size = ImGui::GetFontSize() * canvas_zoom_ * 0.8f; - if (font_size > 5.0f) { - ImVec2 mid = {(w.p0.x + w.p3.x) * 0.5f, (w.p0.y + w.p3.y) * 0.5f}; - ImVec2 text_sz = ImGui::CalcTextSize(w.net_id.c_str()); - float tw = text_sz.x * (font_size / ImGui::GetFontSize()); - float tth = text_sz.y * (font_size / ImGui::GetFontSize()); - float cx = mid.x - tw * 0.5f; - float cy = mid.y - tth * 0.5f; - dl->AddRectFilled({cx - 3, cy - 1}, {cx + tw + 3, cy + tth + 1}, - S.col_label_bg, S.node_rounding); - dl->AddText(nullptr, font_size, {cx, cy}, S.col_label_text, w.net_id.c_str()); - } - } + render_wire(dl, w, canvas_zoom_); + render_wire_label(dl, w, canvas_zoom_); } dl->PopClipRect(); @@ -447,11 +295,13 @@ void Editor2Pane::draw() { drag_was_overlapping_ = false; float pad = S.node_height * 0.5f; for (auto& sel : selected_nodes_) { - auto sel_layout = compute_node_layout(sel, {0,0}, 1.0f); + auto sel_vpm = VisualPinMap::build(sel, find_node_type2(sel->type_id)); + auto sel_layout = compute_node_layout(sel, sel_vpm, {0,0}, 1.0f); for (auto& [oid, oned] : node_editors_) { auto on = oned->node; if (selected_nodes_.count(on)) continue; - auto ol = compute_node_layout(on, {0,0}, 1.0f); + auto on_vpm = VisualPinMap::build(on, find_node_type2(on->type_id)); + auto ol = compute_node_layout(on, on_vpm, {0,0}, 1.0f); if (sel->position.x < on->position.x - pad + ol.width + pad * 2 && sel->position.x + sel_layout.width > on->position.x - pad && sel->position.y < on->position.y - pad + ol.height + pad * 2 && @@ -481,12 +331,14 @@ void Editor2Pane::draw() { if (!drag_was_overlapping_) { float pad = S.node_height * 0.5f; for (auto& sel : selected_nodes_) { - auto sel_layout = compute_node_layout(sel, {0,0}, 1.0f); + auto sel_vpm = VisualPinMap::build(sel, find_node_type2(sel->type_id)); + auto sel_layout = compute_node_layout(sel, sel_vpm, {0,0}, 1.0f); float nx = sel->position.x + dx, ny = sel->position.y + dy; for (auto& [oid, oned] : node_editors_) { auto on = oned->node; if (selected_nodes_.count(on)) continue; - auto ol = compute_node_layout(on, {0,0}, 1.0f); + auto on_vpm = VisualPinMap::build(on, find_node_type2(on->type_id)); + auto ol = compute_node_layout(on, on_vpm, {0,0}, 1.0f); float ox = on->position.x - pad, oy = on->position.y - pad; float ow = ol.width + pad * 2, oh = ol.height + pad * 2; if (nx < ox + ow && nx + sel_layout.width > ox && @@ -520,13 +372,13 @@ void Editor2Pane::draw() { ImVec2 sp0 = {canvas_origin.x + x0 * canvas_zoom_, canvas_origin.y + y0 * canvas_zoom_}; ImVec2 sp1 = {canvas_origin.x + x1 * canvas_zoom_, canvas_origin.y + y1 * canvas_zoom_}; - dl->AddRectFilled(sp0, sp1, IM_COL32(100, 130, 200, 40)); - dl->AddRect(sp0, sp1, IM_COL32(100, 130, 200, 180), 0, 0, 1.5f); + render_selection_rect(dl, sp0, sp1); selected_nodes_.clear(); for (auto& [id, ned] : node_editors_) { auto node = ned->node; - auto layout = compute_node_layout(node, {0,0}, 1.0f); + auto node_vpm = VisualPinMap::build(node, find_node_type2(node->type_id)); + auto layout = compute_node_layout(node, node_vpm, {0,0}, 1.0f); float nx0 = node->position.x, ny0 = node->position.y; float nx1 = nx0 + layout.width, ny1 = ny0 + layout.height; if (nx0 < x1 && nx1 > x0 && ny0 < y1 && ny1 > y0) @@ -539,7 +391,7 @@ void Editor2Pane::draw() { selection_rect_active_ = false; } - // Pan with middle mouse or right mouse + // Pan if (canvas_hovered && ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) { canvas_offset_.x += ImGui::GetIO().MouseDelta.x; canvas_offset_.y += ImGui::GetIO().MouseDelta.y; @@ -549,7 +401,7 @@ void Editor2Pane::draw() { canvas_offset_.y += ImGui::GetIO().MouseDelta.y; } - // Zoom with scroll + // Zoom if (canvas_hovered) { float wheel = ImGui::GetIO().MouseWheel; if (wheel != 0) { @@ -564,501 +416,49 @@ void Editor2Pane::draw() { } } -// ─── Draw a node ─── - -void Editor2Pane::draw_node(ImDrawList* dl, const std::shared_ptr& ned, - ImVec2 canvas_origin) { - auto& node = ned->node; - auto* nt = find_node_type2(node->type_id); - if (!nt) return; - - auto& layout = ned->layout; - auto& pm = ned->pin_mapping; - - // Special nodes: label and error - if (nt->is_special()) { - std::string display; - if (node->parsed_args && !node->parsed_args->empty()) { - auto a = (*node->parsed_args)[0]; - if (auto s = a->as_string()) display = s->value(); - else if (auto e = a->as_expr()) display = e->expr(); - else display = node->args_str(); - } - - float font_size = ImGui::GetFontSize() * canvas_zoom_; - bool is_error = (node->type_id == NodeTypeID::Error); - - if (is_error) { - dl->AddRectFilled(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, - S.col_node_err, S.node_rounding * canvas_zoom_); - dl->AddRect(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, - S.col_err_border, S.node_rounding * canvas_zoom_); - } - - if (font_size > 5.0f) { - ImVec2 text_sz = ImGui::CalcTextSize(display.c_str()); - float tw = text_sz.x * canvas_zoom_; - float cx = layout.pos.x + (layout.width - tw) * 0.5f; - float cy = layout.pos.y + (layout.height - font_size) * 0.5f; - dl->AddText(nullptr, font_size, {cx, cy}, S.col_text, display.c_str()); - } - return; - } - - bool selected = selected_nodes_.count(node); - bool node_hovered = false; - if (auto* ep = std::get_if(&hover_item_)) - node_hovered = (*ep == node); - - bool pin_hovered_on_this = false; - if (auto* pin = std::get_if(&hover_item_)) - pin_hovered_on_this = ((*pin)->node() == node); - else if (auto* add = std::get_if(&hover_item_)) - pin_hovered_on_this = (add->node == node); - bool has_error = ned->has_error; - - ImU32 col = has_error ? S.col_node_err : (selected ? S.col_node_sel : S.col_node); - dl->AddRectFilled(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, - col, S.node_rounding * canvas_zoom_); - dl->AddRect(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, - node_hovered ? S.col_pin_hover : S.col_node_border, S.node_rounding * canvas_zoom_, - 0, node_hovered ? S.highlight_thickness : 1.0f); - - // Text - float font_size = ImGui::GetFontSize() * canvas_zoom_; - if (font_size > 5.0f) { - ImVec2 text_sz = ImGui::CalcTextSize(ned->display_text.c_str()); - float tw = text_sz.x * canvas_zoom_; - float cx = layout.pos.x + (layout.width - tw) * 0.5f; - float cy = layout.pos.y + (layout.height - font_size) * 0.5f; - dl->AddText(nullptr, font_size, {cx, cy}, S.col_text, ned->display_text.c_str()); - } - - // Draw input pins (top) using PinMapping - float pr = S.pin_radius * canvas_zoom_; - for (int i = 0; i < layout.num_in; i++) { - ImVec2 pp = layout.input_pin_pos(i); - - if (pm.is_add_diamond(i)) { - ImU32 pc = S.col_add_pin; - dl->AddQuadFilled({pp.x, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x, pp.y + pr}, {pp.x - pr, pp.y}, pc); - float cr = pr * 0.5f; - float lth = S.add_pin_line * canvas_zoom_; - dl->AddLine({pp.x - cr, pp.y}, {pp.x + cr, pp.y}, S.col_add_pin_fg, lth); - dl->AddLine({pp.x, pp.y - cr}, {pp.x, pp.y + cr}, S.col_add_pin_fg, lth); - continue; - } - - PortKind2 kind = PortKind2::Data; - bool is_input_va = pm.is_input_va(i); - bool is_optional = false; - - if (pm.is_absent_optional(i)) { - int port = pm.absent_port_index(i); - if (auto* pd = nt->input_port(port)) kind = pd->kind; - is_optional = true; - } else if (pm.is_base(i)) { - int port = pm.port_index(i); - if (auto* pd = nt->input_port(port)) { - kind = pd->kind; - is_optional = pd->optional; - } - } else if (is_input_va) { - kind = nt->input_ports_va_args ? nt->input_ports_va_args->kind : PortKind2::Data; - } - - ImU32 pc = pin_color(kind); - if (kind == PortKind2::BangTrigger) { - dl->AddRectFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y + pr}, pc); - } else if (kind == PortKind2::Lambda) { - dl->AddTriangleFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y - pr}, {pp.x, pp.y + pr}, pc); - } else if (is_input_va) { - dl->AddQuadFilled({pp.x, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x, pp.y + pr}, {pp.x - pr, pp.y}, pc); - } else if (is_optional) { - dl->AddQuadFilled({pp.x, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x, pp.y + pr}, {pp.x - pr, pp.y}, pc); - float font_sz = pr * 1.6f; - if (font_sz > 3.0f) { - ImVec2 ts = ImGui::CalcTextSize("?"); - float scale = font_sz / ImGui::GetFontSize(); - dl->AddText(nullptr, font_sz, - {pp.x - ts.x * scale * 0.5f, pp.y - ts.y * scale * 0.5f}, - S.col_opt_pin_fg, "?"); - } - } else { - dl->AddCircleFilled(pp, pr, pc); - } - } - - // Draw output pins (bottom) - int skip_sb = (nt->is_flow()) ? 1 : 0; - int fixed_out = nt->num_outputs; - int rendered_fixed = fixed_out - skip_sb; - for (int i = 0; i < layout.num_out; i++) { - ImVec2 pp = layout.output_pin_pos(i); - int out_idx = i + skip_sb; - bool is_output_va = (i >= rendered_fixed); - - PortKind2 kind = PortKind2::Data; - if (!is_output_va && nt->output_ports && out_idx < nt->num_outputs) - kind = nt->output_ports[out_idx].kind; - else if (is_output_va && nt->output_ports_va_args) - kind = nt->output_ports_va_args->kind; - - ImU32 pc = pin_color(kind); - if (kind == PortKind2::BangNext) { - dl->AddRectFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y + pr}, pc); - } else if (is_output_va) { - dl->AddQuadFilled({pp.x, pp.y + pr}, {pp.x + pr, pp.y}, {pp.x, pp.y - pr}, {pp.x - pr, pp.y}, pc); - } else { - dl->AddCircleFilled(pp, pr, pc); - } - } - - // Flow-only: lambda grab (left) and side-bang (right) - if (nt->is_flow()) { - ImVec2 gp = layout.lambda_grab_pos(); - ImU32 lc = S.col_pin_lambda; - dl->AddTriangleFilled( - {gp.x + pr, gp.y - pr}, - {gp.x - pr, gp.y}, - {gp.x + pr, gp.y + pr}, - lc); - if (node_hovered) { - float ho = S.highlight_offset * canvas_zoom_; - dl->AddTriangle( - {gp.x + pr + ho, gp.y - pr - ho}, - {gp.x - pr - ho, gp.y}, - {gp.x + pr + ho, gp.y + pr + ho}, - S.col_pin_hover, S.highlight_thickness); - } - - ImVec2 bp = {layout.pos.x + layout.width, layout.pos.y + layout.height * 0.5f}; - dl->AddRectFilled({bp.x - pr, bp.y - pr}, {bp.x + pr, bp.y + pr}, S.col_pin_bang); - } - - // Pin/node hover visuals - if (!node_hovered && !pin_hovered_on_this) return; - - float ho = S.highlight_offset * canvas_zoom_; - ImU32 COL_HOVER = S.col_pin_hover; - float ht = S.highlight_thickness; - - enum class PinShape2 { Circle, Square, Diamond, TriangleDown, TriangleLeft }; - auto draw_highlight = [&](ImVec2 pos, PinShape2 shape) { - switch (shape) { - case PinShape2::Circle: dl->AddCircle(pos, pr + ho, COL_HOVER, 0, ht); break; - case PinShape2::Square: dl->AddRect({pos.x-pr-ho,pos.y-pr-ho},{pos.x+pr+ho,pos.y+pr+ho}, COL_HOVER, 0, 0, ht); break; - case PinShape2::Diamond: dl->AddQuad({pos.x,pos.y-pr-ho},{pos.x+pr+ho,pos.y},{pos.x,pos.y+pr+ho},{pos.x-pr-ho,pos.y}, COL_HOVER, ht); break; - case PinShape2::TriangleDown: dl->AddTriangle({pos.x-pr-ho,pos.y-pr-ho},{pos.x+pr+ho,pos.y-pr-ho},{pos.x,pos.y+pr+ho}, COL_HOVER, ht); break; - case PinShape2::TriangleLeft: dl->AddTriangle({pos.x+pr+ho,pos.y-pr-ho},{pos.x-pr-ho,pos.y},{pos.x+pr+ho,pos.y+pr+ho}, COL_HOVER, ht); break; - } - }; - - FlowArg2Ptr hovered_pin = nullptr; - if (auto* pp = std::get_if(&hover_item_)) - hovered_pin = *pp; - - // +diamond hover - if (auto* add_hover = std::get_if(&hover_item_)) { - if (add_hover->node == node) { - for (int i = 0; i < pm.total(); i++) { - if (pm.is_add_diamond(i)) { - ImVec2 pp = layout.input_pin_pos(i); - draw_highlight(pp, PinShape2::Diamond); - if (draw_tooltips_) { - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(S.tooltip_scale); - ImGui::Text("add %s", add_hover->va_port ? add_hover->va_port->name : "arg"); - ImGui::EndTooltip(); - } - return; - } - } - } - } - - if (hovered_pin) { - // Input pins - for (int i = 0; i < pm.total(); i++) { - if (pm.is_add_diamond(i) || pm.is_absent_optional(i)) continue; - FlowArg2Ptr pin_arg = nullptr; - if (pm.is_base(i)) { - int port = pm.port_index(i); - if (node->parsed_args && port < node->parsed_args->size()) - pin_arg = (*node->parsed_args)[port]; - } else if (pm.is_input_va(i)) { - int vi = -(pm.port_index(i) + 1); - if (node->parsed_va_args && vi < node->parsed_va_args->size()) - pin_arg = (*node->parsed_va_args)[vi]; - } else if (pm.is_remap(i)) { - int ri = pm.remap_index(i); - if (ri < (int)node->remaps.size()) pin_arg = node->remaps[ri]; - } - if (pin_arg == hovered_pin) { - ImVec2 pp = layout.input_pin_pos(i); - auto shape = pm.is_input_va(i) ? PinShape2::Diamond : PinShape2::Circle; - if (pm.is_base(i)) { - if (auto* pd = nt->input_port(pm.port_index(i))) { - if (pd->kind == PortKind2::BangTrigger) shape = PinShape2::Square; - else if (pd->kind == PortKind2::Lambda) shape = PinShape2::TriangleDown; - else if (pd->optional) shape = PinShape2::Diamond; - } - } - draw_highlight(pp, shape); - if (draw_tooltips_) { - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(S.tooltip_scale); - if (hovered_pin->port()) - ImGui::Text("%s", hovered_pin->name().c_str()); - else if (pm.is_remap(i)) { - int ri = pm.remap_index(i); - ImGui::Text("$%d", ri); - } - ImGui::EndTooltip(); - } - return; - } - } - // Output pins - { - int hl_skip_sb = nt->is_flow() ? 1 : 0; - int hl_rendered_fixed = fixed_out - hl_skip_sb; - for (int i = 0; i < layout.num_out; i++) { - int out_idx = i + hl_skip_sb; - bool is_output_va = (i >= hl_rendered_fixed); - FlowArg2Ptr out_pin = nullptr; - if (!is_output_va && out_idx < (int)node->outputs.size()) - out_pin = node->outputs[out_idx]; - else if (is_output_va && (i - hl_rendered_fixed) < (int)node->outputs_va_args.size()) - out_pin = node->outputs_va_args[i - hl_rendered_fixed]; - - if (out_pin == hovered_pin) { - ImVec2 pp = layout.output_pin_pos(i); - PinShape2 shape = PinShape2::Circle; - if (!is_output_va && nt->output_ports && out_idx < nt->num_outputs && - nt->output_ports[out_idx].kind == PortKind2::BangNext) - shape = PinShape2::Square; - else if (is_output_va) - shape = PinShape2::Diamond; - draw_highlight(pp, shape); - if (draw_tooltips_) { - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(S.tooltip_scale); - if (hovered_pin->port()) - ImGui::Text("%s", hovered_pin->name().c_str()); - else - ImGui::Text("out%d", i); - ImGui::EndTooltip(); - } - return; - } - } - } - // Side-bang - if (nt->is_flow() && !node->outputs.empty() && node->outputs[0] == hovered_pin) { - ImVec2 bp = {layout.pos.x + layout.width, layout.pos.y + layout.height * 0.5f}; - draw_highlight(bp, PinShape2::Square); - if (draw_tooltips_) { - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(S.tooltip_scale); - ImGui::Text("post_bang"); - ImGui::EndTooltip(); - } - return; - } - } - - // Node body tooltip - if (node_hovered && draw_tooltips_) { - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(S.tooltip_scale); - ImGui::Text("id: %s", node->id().c_str()); - auto show_args = [](const char* label, const ParsedArgs2* pa) { - if (!pa) return; - ImGui::Text("%s (%d):", label, pa->size()); - for (int i = 0; i < pa->size(); i++) { - auto a = (*pa)[i]; - if (auto n = a->as_net()) - ImGui::Text(" [%d] net: %s", i, n->first().c_str()); - else if (auto e = a->as_expr()) - ImGui::Text(" [%d] expr: %s", i, e->expr().c_str()); - else if (auto s = a->as_string()) - ImGui::Text(" [%d] str: %s", i, s->value().c_str()); - else if (auto v = a->as_number()) - ImGui::Text(" [%d] num: %g", i, v->value()); - } - }; - show_args("parsed_args", node->parsed_args.get()); - if (node->parsed_va_args && !node->parsed_va_args->empty()) - show_args("parsed_va_args", node->parsed_va_args.get()); - if (!node->remaps.empty()) { - ImGui::Text("remaps (%d):", (int)node->remaps.size()); - for (int i = 0; i < (int)node->remaps.size(); i++) { - if (auto n = node->remaps[i]->as_net()) - ImGui::Text(" $%d -> %s", i, n->first().c_str()); - } - } - ImGui::EndTooltip(); - } -} - -void Editor2Pane::draw_net(ImDrawList*, const NetBuilderPtr&, ImVec2) { - // Unused — wires drawn via cached_wires_ in draw() -} - // ─── Hover detection ─── -Editor2Pane::HoverItem Editor2Pane::detect_hover(ImVec2 mouse, ImVec2 canvas_origin) { - auto d2 = [](ImVec2 a, ImVec2 b) { return std::sqrt((a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y)); }; - - float best_dist = 1e18f; - HoverItem result = std::monostate{}; - - float pin_bias = S.pin_priority_bias; - - auto try_candidate = [&](float dist, HoverItem candidate) { - if (dist < best_dist) { - best_dist = dist; - result = std::move(candidate); - } - }; - - // Wires - float wire_thresh = S.wire_hit_threshold * canvas_zoom_; - for (auto& w : cached_wires_) { - float d = point_to_bezier_dist(mouse, w.p0, w.p1, w.p2, w.p3); - if (d < wire_thresh) - try_candidate(d, w.entry()); - } - - // Nodes - for (auto it = node_editors_.rbegin(); it != node_editors_.rend(); ++it) { - auto& ned = it->second; - auto& layout = ned->layout; - - float nd; - bool inside = mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && - mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height; - if (inside) { - float dl_ = mouse.x - layout.pos.x; - float dr = layout.pos.x + layout.width - mouse.x; - float dt = mouse.y - layout.pos.y; - float db = layout.pos.y + layout.height - mouse.y; - nd = std::min({dl_, dr, dt, db}); - } else { - float cx = std::clamp(mouse.x, layout.pos.x, layout.pos.x + layout.width); - float cy = std::clamp(mouse.y, layout.pos.y, layout.pos.y + layout.height); - nd = d2(mouse, {cx, cy}); - } - if (nd < S.pin_radius * canvas_zoom_ * S.node_hit_threshold_mul) - try_candidate(nd, BuilderEntryPtr(ned->node)); - } - - // Pins - for (auto it = node_editors_.rbegin(); it != node_editors_.rend(); ++it) { - auto& ned = it->second; - auto& node = ned->node; - auto* nt = find_node_type2(node->type_id); +HoverItem Editor2Pane::detect_hover(ImVec2 mouse, ImVec2 canvas_origin) { + // Build hit targets from node editors + std::vector targets; + targets.reserve(node_editors_.size()); + for (auto& [id, ned] : node_editors_) { + auto* nt = find_node_type2(ned->node->type_id); if (!nt) continue; - auto& layout = ned->layout; - auto& pm = ned->pin_mapping; - float pin_thresh = S.pin_radius * canvas_zoom_ * S.pin_hit_radius_mul; - - // Input pins - for (int i = 0; i < pm.total(); i++) { - if (pm.is_absent_optional(i)) continue; - float pd = d2(mouse, layout.input_pin_pos(i)); - if (pm.is_add_diamond(i)) { - if (pd < pin_thresh && nt->input_ports_va_args) - try_candidate(pd - pin_bias, AddPinHover{node, nt->input_ports_va_args, true}); - continue; - } - if (pd < pin_thresh) { - FlowArg2Ptr pin_arg = nullptr; - if (pm.is_base(i)) { - int port = pm.port_index(i); - if (node->parsed_args && port < node->parsed_args->size()) - pin_arg = (*node->parsed_args)[port]; - } else if (pm.is_input_va(i)) { - int vi = -(pm.port_index(i) + 1); - if (node->parsed_va_args && vi < node->parsed_va_args->size()) - pin_arg = (*node->parsed_va_args)[vi]; - } else if (pm.is_remap(i)) { - int ri = pm.remap_index(i); - if (ri < (int)node->remaps.size()) - pin_arg = node->remaps[ri]; - } - if (pin_arg) try_candidate(pd - pin_bias, pin_arg); - } - } - - // Output pins - { - int skip_sb = nt->is_flow() ? 1 : 0; - int rendered_fixed = nt->num_outputs - skip_sb; - for (int i = 0; i < layout.num_out; i++) { - float pd = d2(mouse, layout.output_pin_pos(i)); - if (pd < pin_thresh) { - int out_idx = i + skip_sb; - bool is_va = (i >= rendered_fixed); - if (!is_va && out_idx < (int)node->outputs.size() && node->outputs[out_idx]) - try_candidate(pd - pin_bias, node->outputs[out_idx]); - else if (is_va) { - int vi = i - rendered_fixed; - if (vi < (int)node->outputs_va_args.size() && node->outputs_va_args[vi]) - try_candidate(pd - pin_bias, node->outputs_va_args[vi]); - } - } - } - } + targets.push_back({ned->node, nt, &ned->layout, &ned->vpm}); + } - // Lambda grab - if (nt->is_flow()) { - float pd = d2(mouse, layout.lambda_grab_pos()); - if (pd < pin_thresh) - try_candidate(pd - pin_bias, BuilderEntryPtr(node)); - } + // Test all 3 categories + auto wire_hit = hit_test_wires(mouse, cached_wires_, canvas_zoom_); + auto node_hit = hit_test_node_bodies(mouse, targets, canvas_zoom_); + auto pin_hit = hit_test_pins(mouse, targets, canvas_zoom_); - // Side-bang - if (nt->is_flow()) { - ImVec2 bp = {layout.pos.x + layout.width, layout.pos.y + layout.height * 0.5f}; - float pd = d2(mouse, bp); - if (pd < pin_thresh) { - if (node->outputs.empty() || !node->outputs[0]) - throw std::logic_error("Flow node '" + node->id() + "' missing side-bang output[0]"); - try_candidate(pd - pin_bias, node->outputs[0]); - } - } - } + // Pick winner (pins have bias built into their distance) + HitResult best = wire_hit; + if (node_hit.distance < best.distance) best = node_hit; + if (pin_hit.distance < best.distance) best = pin_hit; - return result; + return best.item; } -// ─── Hover effects + tooltips ─── +// ─── Hover effects ─── -void Editor2Pane::draw_hover_effects( - ImDrawList* dl, ImVec2 canvas_origin, const HoverItem& hover) -{ +void Editor2Pane::draw_hover_effects(ImDrawList* dl, ImVec2 canvas_origin, const HoverItem& hover) { if (std::holds_alternative(hover)) return; - float th_wire = (S.wire_thickness + 2.0f) * canvas_zoom_; - FlowNodeBuilderPtr hover_node = nullptr; BuilderEntryPtr hover_entry = nullptr; - FlowArg2Ptr hover_pin = nullptr; if (auto* ep = std::get_if(&hover)) { hover_entry = *ep; hover_node = hover_entry ? hover_entry->as_node() : nullptr; - } else if (auto* pp = std::get_if(&hover)) { - hover_pin = *pp; } // Node hovered: highlight lambda wires capturing it if (hover_node) { for (auto& w : cached_wires_) { if (w.is_lambda() && w.entry() == hover_node) - dl->AddBezierCubic(w.p0, w.p1, w.p2, w.p3, S.col_pin_hover, th_wire); + render_wire_highlight(dl, w, canvas_zoom_); } } @@ -1066,7 +466,7 @@ void Editor2Pane::draw_hover_effects( if (hover_entry && hover_entry->as_net()) { for (auto& w : cached_wires_) { if (w.entry() == hover_entry) - dl->AddBezierCubic(w.p0, w.p1, w.p2, w.p3, S.col_pin_hover, th_wire); + render_wire_highlight(dl, w, canvas_zoom_); } if (draw_tooltips_) { for (auto& w : cached_wires_) { diff --git a/src/attoflow/editor2.h b/src/attoflow/editor2.h index f6059c9..775b6ff 100644 --- a/src/attoflow/editor2.h +++ b/src/attoflow/editor2.h @@ -1,110 +1,11 @@ #pragma once -#include "atto/graph_builder.h" +#include "node_renderer.h" #include "atto/graph_editor_interfaces.h" -#include "atto/node_types2.h" -#include "imgui.h" #include #include #include #include -// ─── Style (extern for use in layout computation) ─── - -struct Editor2Style { - float node_min_width = 80.0f; - float node_height = 40.0f; - float pin_radius = 5.0f; - float pin_spacing = 16.0f; - float node_rounding = 4.0f; - float grid_step = 20.0f; - - float wire_thickness = 2.5f; - float node_border = 1.0f; - float highlight_offset = 2.0f; - float highlight_thickness = 2.0f; - float add_pin_line = 1.5f; - - float pin_hit_radius_mul = 2.5f; - float wire_hit_threshold = 30.0f; - float node_hit_threshold_mul = 6.f; - float dismiss_radius = 20.0f; - float pin_priority_bias = 1e6f; - - ImU32 col_bg = IM_COL32(30, 30, 40, 255); - ImU32 col_grid = IM_COL32(50, 50, 60, 255); - - ImU32 col_node = IM_COL32(50, 55, 75, 220); - ImU32 col_node_sel = IM_COL32(80, 90, 130, 255); - ImU32 col_node_err = IM_COL32(130, 40, 40, 220); - ImU32 col_node_border = IM_COL32(80, 80, 100, 255); - ImU32 col_err_border = IM_COL32(255, 80, 80, 255); - ImU32 col_text = IM_COL32(220, 220, 220, 255); - - ImU32 col_pin_data = IM_COL32(100, 200, 100, 255); - ImU32 col_pin_bang = IM_COL32(255, 200, 80, 255); - ImU32 col_pin_lambda = IM_COL32(180, 130, 255, 255); - ImU32 col_pin_hover = IM_COL32(255, 255, 255, 255); - ImU32 col_add_pin = IM_COL32(120, 120, 140, 180); - ImU32 col_add_pin_fg = IM_COL32(200, 200, 220, 220); - ImU32 col_opt_pin_fg = IM_COL32(30, 30, 40, 255); - - ImU32 col_wire = IM_COL32(200, 200, 100, 200); - ImU32 col_wire_named = IM_COL32(200, 200, 100, 120); - ImU32 col_wire_lambda = IM_COL32(180, 130, 255, 200); - - ImU32 col_label_bg = IM_COL32(30, 30, 40, 200); - ImU32 col_label_text = IM_COL32(180, 220, 255, 255); - - float tooltip_scale = 1.0f; -}; - -extern Editor2Style S; - -// ─── PinMapping: maps visible pin index → port descriptor ─── - -struct PinMapping { - std::vector pin_to_port; - int base_count = 0; - int va_count = 0; - int add_pin_pos = -1; - bool has_input_va = false; - - static PinMapping build(const FlowNodeBuilderPtr& node, const NodeType2* nt); - - int total() const { return (int)pin_to_port.size(); } - bool is_base(int pin) const { return pin < base_count; } - bool is_absent_optional(int pin) const { return pin < base_count && pin_to_port[pin] <= -3000; } - int absent_port_index(int pin) const { return -(pin_to_port[pin] + 3000); } - bool is_input_va(int pin) const { return pin >= base_count && pin < base_count + va_count; } - bool is_add_diamond(int pin) const { return pin == add_pin_pos; } - bool is_remap(int pin) const { return pin >= base_count + va_count + (has_input_va ? 1 : 0); } - int port_index(int pin) const { return pin_to_port[pin]; } - int remap_index(int pin) const { return -(pin_to_port[pin] + 2000); } -}; - -// ─── NodeLayout: computed screen-space layout for a node ─── - -struct NodeLayout { - ImVec2 pos; - float width; - float height; - int num_in; - int num_out; - float zoom; - - ImVec2 input_pin_pos(int i) const { - return {pos.x + (i + 0.5f) * S.pin_spacing * zoom, pos.y}; - } - ImVec2 output_pin_pos(int i) const { - return {pos.x + (i + 0.5f) * S.pin_spacing * zoom, pos.y + height}; - } - ImVec2 lambda_grab_pos() const { - return {pos.x, pos.y + height * 0.5f}; - } -}; - -NodeLayout compute_node_layout(const FlowNodeBuilderPtr& node, ImVec2 canvas_origin, float zoom); - // ─── Forward declaration ─── class Editor2Pane; @@ -115,7 +16,7 @@ struct NodeEditorImpl : INodeEditor, std::enable_shared_from_this { public: - // Load an .atto file (instrument@atto:0 format) bool load(const std::string& path); - - // Draw the pane into the current ImGui context void draw(); bool is_loaded() const { return gb_ != nullptr; } @@ -190,7 +88,6 @@ class Editor2Pane : public IGraphEditor, public std::enable_shared_from_this net_added(const NodeId& id, const std::shared_ptr& net) override; void net_removed(const NodeId& id) override; - // Mark wires for rebuild (called by editor impls) void invalidate_wires() { wires_dirty_ = true; } private: @@ -203,12 +100,6 @@ class Editor2Pane : public IGraphEditor, public std::enable_shared_from_this; HoverItem hover_item_; bool draw_tooltips_ = true; std::set selected_nodes_; @@ -216,36 +107,16 @@ class Editor2Pane : public IGraphEditor, public std::enable_shared_from_this> node_editors_; std::map> net_editors_; - // Wire info for hover hit-testing - struct WireInfo { - BuilderEntryPtr entry_; - ImVec2 p0, p1, p2, p3; - NodeId src_id, dst_id, net_id; - BuilderEntryPtr entry() const { return entry_; } - bool is_lambda() const { return entry_ && entry_->is(IdCategory::Node); } - }; - + // Wire cache std::vector cached_wires_; bool wires_dirty_ = true; void rebuild_wires(ImVec2 canvas_origin); - - // Hover detection — returns best hover match HoverItem detect_hover(ImVec2 mouse, ImVec2 canvas_origin); - - // Tooltip + highlight drawing (driven by hover_item parameter) - void draw_hover_effects(ImDrawList* dl, ImVec2 canvas_origin, - const HoverItem& hover); - - // Drawing helpers - void draw_node(ImDrawList* dl, const std::shared_ptr& ned, - ImVec2 canvas_origin); - void draw_net(ImDrawList* dl, const NetBuilderPtr& net, - ImVec2 canvas_origin); + void draw_hover_effects(ImDrawList* dl, ImVec2 canvas_origin, const HoverItem& hover); }; diff --git a/src/attoflow/node_renderer.cpp b/src/attoflow/node_renderer.cpp new file mode 100644 index 0000000..7b54fab --- /dev/null +++ b/src/attoflow/node_renderer.cpp @@ -0,0 +1,588 @@ +#include "node_renderer.h" +#include +#include + +// ─── Style (global instance) ─── + +Editor2Style S; + +// ─── Geometry helpers ─── + +float point_to_bezier_dist(ImVec2 p, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3) { + float min_d2 = 1e18f; + for (int i = 0; i <= 20; i++) { + float t = i / 20.0f; + float u = 1.0f - t; + float x = u*u*u*p0.x + 3*u*u*t*p1.x + 3*u*t*t*p2.x + t*t*t*p3.x; + float y = u*u*u*p0.y + 3*u*u*t*p1.y + 3*u*t*t*p2.y + t*t*t*p3.y; + float dx = p.x - x, dy = p.y - y; + float d2 = dx*dx + dy*dy; + if (d2 < min_d2) min_d2 = d2; + } + return std::sqrt(min_d2); +} + +static float dist2d(ImVec2 a, ImVec2 b) { + return std::sqrt((a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y)); +} + +ImU32 pin_color(PortKind2 kind) { + switch (kind) { + case PortKind2::BangTrigger: + case PortKind2::BangNext: return S.col_pin_bang; + case PortKind2::Lambda: return S.col_pin_lambda; + default: return S.col_pin_data; + } +} + +// ─── VisualPinMap::build ─── + +VisualPinMap VisualPinMap::build(const FlowNodeBuilderPtr& node, const NodeType2* nt) { + VisualPinMap vpm; + if (!nt) return vpm; + + vpm.is_flow = nt->is_flow(); + bool has_input_va = nt->input_ports_va_args != nullptr; + + // Input pins: base args (only Net kind get pins) + int parsed_size = node->parsed_args ? (int)node->parsed_args->size() : 0; + if (node->parsed_args) { + for (int i = 0; i < parsed_size; i++) { + auto arg = (*node->parsed_args)[i]; + if (arg->is(ArgKind::Net)) { + PortKind2 pk = PortKind2::Data; + bool opt = false; + const PortDesc2* pd = nt->input_port(i); + if (pd) { pk = pd->kind; opt = pd->optional; } + vpm.inputs.push_back({VisualPinKind::Base, arg, pd, pk, opt}); + } + } + } + // Absent trailing optional ports + for (int i = parsed_size; i < nt->total_inputs(); i++) { + if (i >= nt->num_inputs) { + const PortDesc2* pd = nt->input_port(i); + PortKind2 pk = pd ? pd->kind : PortKind2::Data; + vpm.inputs.push_back({VisualPinKind::AbsentOptional, nullptr, pd, pk, true}); + } + } + // Va_args + if (node->parsed_va_args) { + PortKind2 va_kind = nt->input_ports_va_args ? nt->input_ports_va_args->kind : PortKind2::Data; + for (int i = 0; i < (int)node->parsed_va_args->size(); i++) { + auto arg = (*node->parsed_va_args)[i]; + if (arg->is(ArgKind::Net)) { + vpm.inputs.push_back({VisualPinKind::VaArg, arg, nt->input_ports_va_args, va_kind, false}); + } + } + } + // +diamond + if (has_input_va) { + vpm.add_diamond_va_port = nt->input_ports_va_args; + vpm.inputs.push_back({VisualPinKind::AddDiamond, nullptr, nt->input_ports_va_args, PortKind2::Data, false}); + } + // Remaps + for (int i = 0; i < (int)node->remaps.size(); i++) { + vpm.inputs.push_back({VisualPinKind::Remap, node->remaps[i], nullptr, PortKind2::Data, false}); + } + + // Output pins + int skip_sb = nt->is_flow() ? 1 : 0; + if (skip_sb && !node->outputs.empty()) { + vpm.has_side_bang = true; + vpm.side_bang_arg = node->outputs[0]; + } + // Fixed outputs (skipping side-bang) + for (int i = skip_sb; i < (int)node->outputs.size(); i++) { + PortKind2 pk = PortKind2::Data; + const PortDesc2* pd = (nt->output_ports && i < nt->num_outputs) ? &nt->output_ports[i] : nullptr; + if (pd) pk = pd->kind; + vpm.outputs.push_back({VisualPinKind::Base, node->outputs[i], pd, pk, false}); + } + // Va_args outputs + PortKind2 out_va_kind = nt->output_ports_va_args ? nt->output_ports_va_args->kind : PortKind2::Data; + for (int i = 0; i < (int)node->outputs_va_args.size(); i++) { + vpm.outputs.push_back({VisualPinKind::VaArg, node->outputs_va_args[i], nt->output_ports_va_args, out_va_kind, false}); + } + + return vpm; +} + +// ─── compute_node_layout ─── + +NodeLayout compute_node_layout(const FlowNodeBuilderPtr& node, const VisualPinMap& vpm, + ImVec2 canvas_origin, float zoom) { + auto* nt = find_node_type2(node->type_id); + std::string display = nt ? nt->name : "?"; + std::string args = node->args_str(); + if (!args.empty()) display += " " + args; + + ImVec2 text_sz = ImGui::CalcTextSize(display.c_str()); + float text_w = text_sz.x * zoom + 16.0f * zoom; + + int num_in = (int)vpm.inputs.size(); + int num_out = (int)vpm.outputs.size(); + + float pin_w_top = std::max(0, num_in) * S.pin_spacing * zoom; + float pin_w_bot = std::max(0, num_out) * S.pin_spacing * zoom; + float node_w = std::max({S.node_min_width * zoom, text_w, pin_w_top, pin_w_bot}); + float node_h = S.node_height * zoom; + + ImVec2 pos = {canvas_origin.x + node->position.x * zoom, + canvas_origin.y + node->position.y * zoom}; + + return {pos, node_w, node_h, num_in, num_out, zoom}; +} + +// ─── render_background ─── + +void render_background(ImDrawList* dl, ImVec2 canvas_p0, ImVec2 canvas_sz, + ImVec2 canvas_offset, float zoom) { + dl->AddRectFilled(canvas_p0, v2add(canvas_p0, canvas_sz), S.col_bg); + + float grid_step = S.grid_step * zoom; + if (grid_step > 5.0f) { + for (float x = fmodf(canvas_offset.x, grid_step); x < canvas_sz.x; x += grid_step) + dl->AddLine({canvas_p0.x + x, canvas_p0.y}, {canvas_p0.x + x, canvas_p0.y + canvas_sz.y}, S.col_grid); + for (float y = fmodf(canvas_offset.y, grid_step); y < canvas_sz.y; y += grid_step) + dl->AddLine({canvas_p0.x, canvas_p0.y + y}, {canvas_p0.x + canvas_sz.x, canvas_p0.y + y}, S.col_grid); + } +} + +// ─── render_node ─── + +void render_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, const NodeType2* nt, + const NodeLayout& layout, const VisualPinMap& vpm, + const std::string& display_text, const NodeRenderState& state, + float zoom, bool draw_tooltips) { + if (!nt) return; + + float pr = S.pin_radius * zoom; + + // Special nodes: label and error + if (nt->is_special()) { + std::string display; + if (node->parsed_args && !node->parsed_args->empty()) { + auto a = (*node->parsed_args)[0]; + if (auto s = a->as_string()) display = s->value(); + else if (auto e = a->as_expr()) display = e->expr(); + else display = node->args_str(); + } + + float font_size = ImGui::GetFontSize() * zoom; + bool is_error = (node->type_id == NodeTypeID::Error); + + if (is_error) { + dl->AddRectFilled(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, + S.col_node_err, S.node_rounding * zoom); + dl->AddRect(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, + S.col_err_border, S.node_rounding * zoom); + } + + if (font_size > 5.0f) { + ImVec2 text_sz = ImGui::CalcTextSize(display.c_str()); + float tw = text_sz.x * zoom; + float cx = layout.pos.x + (layout.width - tw) * 0.5f; + float cy = layout.pos.y + (layout.height - font_size) * 0.5f; + dl->AddText(nullptr, font_size, {cx, cy}, S.col_text, display.c_str()); + } + return; + } + + // Node body + ImU32 col = state.selected ? S.col_node_sel : S.col_node; + if (!node->error.empty()) col = S.col_node_err; + dl->AddRectFilled(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, + col, S.node_rounding * zoom); + dl->AddRect(layout.pos, {layout.pos.x + layout.width, layout.pos.y + layout.height}, + state.node_hovered ? S.col_pin_hover : S.col_node_border, S.node_rounding * zoom, + 0, state.node_hovered ? S.highlight_thickness : 1.0f); + + // Text + float font_size = ImGui::GetFontSize() * zoom; + if (font_size > 5.0f) { + ImVec2 text_sz = ImGui::CalcTextSize(display_text.c_str()); + float tw = text_sz.x * zoom; + float cx = layout.pos.x + (layout.width - tw) * 0.5f; + float cy = layout.pos.y + (layout.height - font_size) * 0.5f; + dl->AddText(nullptr, font_size, {cx, cy}, S.col_text, display_text.c_str()); + } + + // ─── Input pins ─── + for (int i = 0; i < (int)vpm.inputs.size(); i++) { + auto& pin = vpm.inputs[i]; + ImVec2 pp = layout.input_pin_pos(i); + + if (pin.kind == VisualPinKind::AddDiamond) { + ImU32 pc = S.col_add_pin; + dl->AddQuadFilled({pp.x, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x, pp.y + pr}, {pp.x - pr, pp.y}, pc); + float cr = pr * 0.5f; + float lth = S.add_pin_line * zoom; + dl->AddLine({pp.x - cr, pp.y}, {pp.x + cr, pp.y}, S.col_add_pin_fg, lth); + dl->AddLine({pp.x, pp.y - cr}, {pp.x, pp.y + cr}, S.col_add_pin_fg, lth); + continue; + } + + ImU32 pc = pin_color(pin.port_kind); + if (pin.kind == VisualPinKind::AbsentOptional || (pin.is_optional && pin.kind == VisualPinKind::Base)) { + dl->AddQuadFilled({pp.x, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x, pp.y + pr}, {pp.x - pr, pp.y}, pc); + if (pin.kind == VisualPinKind::AbsentOptional) { + float font_sz = pr * 1.6f; + if (font_sz > 3.0f) { + ImVec2 ts = ImGui::CalcTextSize("?"); + float scale = font_sz / ImGui::GetFontSize(); + dl->AddText(nullptr, font_sz, + {pp.x - ts.x * scale * 0.5f, pp.y - ts.y * scale * 0.5f}, + S.col_opt_pin_fg, "?"); + } + } + } else if (pin.port_kind == PortKind2::BangTrigger) { + dl->AddRectFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y + pr}, pc); + } else if (pin.port_kind == PortKind2::Lambda) { + dl->AddTriangleFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y - pr}, {pp.x, pp.y + pr}, pc); + } else if (pin.kind == VisualPinKind::VaArg) { + dl->AddQuadFilled({pp.x, pp.y - pr}, {pp.x + pr, pp.y}, {pp.x, pp.y + pr}, {pp.x - pr, pp.y}, pc); + } else { + dl->AddCircleFilled(pp, pr, pc); + } + } + + // ─── Output pins ─── + for (int i = 0; i < (int)vpm.outputs.size(); i++) { + auto& pin = vpm.outputs[i]; + ImVec2 pp = layout.output_pin_pos(i); + + ImU32 pc = pin_color(pin.port_kind); + if (pin.port_kind == PortKind2::BangNext) { + dl->AddRectFilled({pp.x - pr, pp.y - pr}, {pp.x + pr, pp.y + pr}, pc); + } else if (pin.kind == VisualPinKind::VaArg) { + dl->AddQuadFilled({pp.x, pp.y + pr}, {pp.x + pr, pp.y}, {pp.x, pp.y - pr}, {pp.x - pr, pp.y}, pc); + } else { + dl->AddCircleFilled(pp, pr, pc); + } + } + + // ─── Flow-only: lambda grab (left) and side-bang (right) ─── + if (vpm.is_flow) { + ImVec2 gp = layout.lambda_grab_pos(); + dl->AddTriangleFilled( + {gp.x + pr, gp.y - pr}, {gp.x - pr, gp.y}, {gp.x + pr, gp.y + pr}, + S.col_pin_lambda); + if (state.node_hovered) { + float ho = S.highlight_offset * zoom; + dl->AddTriangle( + {gp.x + pr + ho, gp.y - pr - ho}, {gp.x - pr - ho, gp.y}, {gp.x + pr + ho, gp.y + pr + ho}, + S.col_pin_hover, S.highlight_thickness); + } + + ImVec2 bp = layout.side_bang_pos(); + dl->AddRectFilled({bp.x - pr, bp.y - pr}, {bp.x + pr, bp.y + pr}, S.col_pin_bang); + } + + // ─── Hover highlights ─── + if (!state.node_hovered && !state.pin_hovered_on_this) return; + + float ho = S.highlight_offset * zoom; + ImU32 COL_HOVER = S.col_pin_hover; + float ht = S.highlight_thickness; + + enum class PinShape { Circle, Square, Diamond, TriangleDown, TriangleLeft }; + auto draw_highlight = [&](ImVec2 pos, PinShape shape) { + switch (shape) { + case PinShape::Circle: dl->AddCircle(pos, pr + ho, COL_HOVER, 0, ht); break; + case PinShape::Square: dl->AddRect({pos.x-pr-ho,pos.y-pr-ho},{pos.x+pr+ho,pos.y+pr+ho}, COL_HOVER, 0, 0, ht); break; + case PinShape::Diamond: dl->AddQuad({pos.x,pos.y-pr-ho},{pos.x+pr+ho,pos.y},{pos.x,pos.y+pr+ho},{pos.x-pr-ho,pos.y}, COL_HOVER, ht); break; + case PinShape::TriangleDown: dl->AddTriangle({pos.x-pr-ho,pos.y-pr-ho},{pos.x+pr+ho,pos.y-pr-ho},{pos.x,pos.y+pr+ho}, COL_HOVER, ht); break; + case PinShape::TriangleLeft: dl->AddTriangle({pos.x+pr+ho,pos.y-pr-ho},{pos.x-pr-ho,pos.y},{pos.x+pr+ho,pos.y+pr+ho}, COL_HOVER, ht); break; + } + }; + + auto pin_shape_for = [](const VisualPin& pin) -> PinShape { + if (pin.kind == VisualPinKind::VaArg || pin.kind == VisualPinKind::AddDiamond) return PinShape::Diamond; + if (pin.is_optional || pin.kind == VisualPinKind::AbsentOptional) return PinShape::Diamond; + if (pin.port_kind == PortKind2::BangTrigger || pin.port_kind == PortKind2::BangNext) return PinShape::Square; + if (pin.port_kind == PortKind2::Lambda) return PinShape::TriangleDown; + return PinShape::Circle; + }; + + // +diamond hover + if (state.add_pin_hover && state.add_pin_hover->node == node) { + for (int i = 0; i < (int)vpm.inputs.size(); i++) { + if (vpm.inputs[i].kind == VisualPinKind::AddDiamond) { + draw_highlight(layout.input_pin_pos(i), PinShape::Diamond); + if (draw_tooltips) { + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + ImGui::Text("add %s", state.add_pin_hover->va_port ? state.add_pin_hover->va_port->name : "arg"); + ImGui::EndTooltip(); + } + return; + } + } + } + + if (state.hovered_pin) { + // Input pins + for (int i = 0; i < (int)vpm.inputs.size(); i++) { + auto& pin = vpm.inputs[i]; + if (pin.kind == VisualPinKind::AddDiamond || pin.kind == VisualPinKind::AbsentOptional) continue; + if (pin.arg == state.hovered_pin) { + draw_highlight(layout.input_pin_pos(i), pin_shape_for(pin)); + if (draw_tooltips) { + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + if (pin.arg->port()) + ImGui::Text("%s", pin.arg->name().c_str()); + else if (pin.kind == VisualPinKind::Remap) + ImGui::Text("$%d", pin.arg->remap_idx()); + ImGui::EndTooltip(); + } + return; + } + } + // Output pins + for (int i = 0; i < (int)vpm.outputs.size(); i++) { + auto& pin = vpm.outputs[i]; + if (pin.arg == state.hovered_pin) { + draw_highlight(layout.output_pin_pos(i), pin_shape_for(pin)); + if (draw_tooltips) { + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + if (pin.arg->port()) + ImGui::Text("%s", pin.arg->name().c_str()); + else + ImGui::Text("out%d", i); + ImGui::EndTooltip(); + } + return; + } + } + // Side-bang + if (vpm.has_side_bang && vpm.side_bang_arg == state.hovered_pin) { + draw_highlight(layout.side_bang_pos(), PinShape::Square); + if (draw_tooltips) { + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + ImGui::Text("post_bang"); + ImGui::EndTooltip(); + } + return; + } + } + + // Node body tooltip + if (state.node_hovered && draw_tooltips) { + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + ImGui::Text("id: %s", node->id().c_str()); + auto show_args = [](const char* label, const ParsedArgs2* pa) { + if (!pa) return; + ImGui::Text("%s (%d):", label, pa->size()); + for (int i = 0; i < pa->size(); i++) { + auto a = (*pa)[i]; + if (auto n = a->as_net()) + ImGui::Text(" [%d] net: %s", i, n->first().c_str()); + else if (auto e = a->as_expr()) + ImGui::Text(" [%d] expr: %s", i, e->expr().c_str()); + else if (auto s = a->as_string()) + ImGui::Text(" [%d] str: %s", i, s->value().c_str()); + else if (auto v = a->as_number()) + ImGui::Text(" [%d] num: %g", i, v->value()); + } + }; + show_args("parsed_args", node->parsed_args.get()); + if (node->parsed_va_args && !node->parsed_va_args->empty()) + show_args("parsed_va_args", node->parsed_va_args.get()); + if (!node->remaps.empty()) { + ImGui::Text("remaps (%d):", (int)node->remaps.size()); + for (int i = 0; i < (int)node->remaps.size(); i++) { + if (auto n = node->remaps[i]->as_net()) + ImGui::Text(" $%d -> %s", i, n->first().c_str()); + } + } + ImGui::EndTooltip(); + } +} + +// ─── Wire rendering ─── + +void render_wire(ImDrawList* dl, const WireInfo& w, float zoom) { + bool is_lambda = w.is_lambda(); + bool named = false; + if (!is_lambda) { + if (auto net = w.entry_->as_net()) + named = !net->auto_wire(); + } + ImU32 wire_col = is_lambda ? S.col_wire_lambda : (named ? S.col_wire_named : S.col_wire); + float th = S.wire_thickness * zoom; + dl->AddBezierCubic(w.p0, w.p1, w.p2, w.p3, wire_col, th); +} + +void render_wire_label(ImDrawList* dl, const WireInfo& w, float zoom) { + if (w.is_lambda()) return; + auto net = w.entry_ ? w.entry_->as_net() : nullptr; + if (!net || net->auto_wire()) return; + + float font_size = ImGui::GetFontSize() * zoom * 0.8f; + if (font_size <= 5.0f) return; + + ImVec2 mid = {(w.p0.x + w.p3.x) * 0.5f, (w.p0.y + w.p3.y) * 0.5f}; + ImVec2 text_sz = ImGui::CalcTextSize(w.net_id.c_str()); + float tw = text_sz.x * (font_size / ImGui::GetFontSize()); + float tth = text_sz.y * (font_size / ImGui::GetFontSize()); + float cx = mid.x - tw * 0.5f; + float cy = mid.y - tth * 0.5f; + dl->AddRectFilled({cx - 3, cy - 1}, {cx + tw + 3, cy + tth + 1}, + S.col_label_bg, S.node_rounding); + dl->AddText(nullptr, font_size, {cx, cy}, S.col_label_text, w.net_id.c_str()); +} + +void render_wire_highlight(ImDrawList* dl, const WireInfo& w, float zoom) { + float th = (S.wire_thickness + 2.0f) * zoom; + dl->AddBezierCubic(w.p0, w.p1, w.p2, w.p3, S.col_pin_hover, th); +} + +void render_selection_rect(ImDrawList* dl, ImVec2 p0, ImVec2 p1) { + dl->AddRectFilled(p0, p1, IM_COL32(100, 130, 200, 40)); + dl->AddRect(p0, p1, IM_COL32(100, 130, 200, 180), 0, 0, 1.5f); +} + +// ─── compute_wire_geometry ─── + +WireInfo compute_wire_geometry(ImVec2 from, ImVec2 to, bool is_lambda, bool is_side_bang, + float zoom, const BuilderEntryPtr& entry, + const NodeId& src_id, const NodeId& dst_id, const NodeId& net_id) { + ImVec2 cp1, cp2; + if (is_lambda) { + float dx = std::max(std::abs(to.x - from.x) * 0.5f, 30.0f * zoom); + float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * zoom); + cp1 = {from.x - dx, from.y}; + cp2 = {to.x, to.y - dy}; + } else if (is_side_bang) { + float dx = std::max(std::abs(to.x - from.x) * 0.5f, 30.0f * zoom); + float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * zoom); + cp1 = {from.x + dx, from.y}; + cp2 = {to.x, to.y - dy}; + } else { + float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * zoom); + cp1 = {from.x, from.y + dy}; + cp2 = {to.x, to.y - dy}; + } + return {entry, from, cp1, cp2, to, src_id, dst_id, net_id}; +} + +// ─── Hit-testing ─── + +HitResult hit_test_wires(ImVec2 mouse, const std::vector& wires, float zoom) { + HitResult best; + float wire_thresh = S.wire_hit_threshold * zoom; + for (auto& w : wires) { + float d = point_to_bezier_dist(mouse, w.p0, w.p1, w.p2, w.p3); + if (d < wire_thresh && d < best.distance) { + best.distance = d; + best.item = w.entry(); + } + } + return best; +} + +HitResult hit_test_node_bodies(ImVec2 mouse, const std::vector& nodes, float zoom) { + HitResult best; + for (auto it = nodes.rbegin(); it != nodes.rend(); ++it) { + auto& layout = *it->layout; + float nd; + bool inside = mouse.x >= layout.pos.x && mouse.x <= layout.pos.x + layout.width && + mouse.y >= layout.pos.y && mouse.y <= layout.pos.y + layout.height; + if (inside) { + float dl_ = mouse.x - layout.pos.x; + float dr = layout.pos.x + layout.width - mouse.x; + float dt = mouse.y - layout.pos.y; + float db = layout.pos.y + layout.height - mouse.y; + nd = std::min({dl_, dr, dt, db}); + } else { + float cx = std::clamp(mouse.x, layout.pos.x, layout.pos.x + layout.width); + float cy = std::clamp(mouse.y, layout.pos.y, layout.pos.y + layout.height); + nd = dist2d(mouse, {cx, cy}); + } + if (nd < S.pin_radius * zoom * S.node_hit_threshold_mul && nd < best.distance) { + best.distance = nd; + best.item = BuilderEntryPtr(it->node); + } + } + return best; +} + +HitResult hit_test_pins(ImVec2 mouse, const std::vector& nodes, float zoom) { + HitResult best; + float pin_thresh = S.pin_radius * zoom * S.pin_hit_radius_mul; + float pin_bias = S.pin_priority_bias; + + for (auto it = nodes.rbegin(); it != nodes.rend(); ++it) { + auto& node = it->node; + auto& layout = *it->layout; + auto& vpm = *it->vpm; + + // Input pins + for (int i = 0; i < (int)vpm.inputs.size(); i++) { + auto& pin = vpm.inputs[i]; + if (pin.kind == VisualPinKind::AbsentOptional) continue; + float pd = dist2d(mouse, layout.input_pin_pos(i)); + if (pin.kind == VisualPinKind::AddDiamond) { + if (pd < pin_thresh && vpm.add_diamond_va_port) { + float biased = pd - pin_bias; + if (biased < best.distance) { + best.distance = biased; + best.item = AddPinHover{node, vpm.add_diamond_va_port, true}; + } + } + continue; + } + if (pd < pin_thresh && pin.arg) { + float biased = pd - pin_bias; + if (biased < best.distance) { + best.distance = biased; + best.item = pin.arg; + } + } + } + + // Output pins + for (int i = 0; i < (int)vpm.outputs.size(); i++) { + auto& pin = vpm.outputs[i]; + float pd = dist2d(mouse, layout.output_pin_pos(i)); + if (pd < pin_thresh && pin.arg) { + float biased = pd - pin_bias; + if (biased < best.distance) { + best.distance = biased; + best.item = pin.arg; + } + } + } + + // Lambda grab → node itself + if (vpm.is_flow) { + float pd = dist2d(mouse, layout.lambda_grab_pos()); + if (pd < pin_thresh) { + float biased = pd - pin_bias; + if (biased < best.distance) { + best.distance = biased; + best.item = BuilderEntryPtr(node); + } + } + } + + // Side-bang + if (vpm.has_side_bang && vpm.side_bang_arg) { + float pd = dist2d(mouse, layout.side_bang_pos()); + if (pd < pin_thresh) { + float biased = pd - pin_bias; + if (biased < best.distance) { + best.distance = biased; + best.item = vpm.side_bang_arg; + } + } + } + } + + return best; +} diff --git a/src/attoflow/node_renderer.h b/src/attoflow/node_renderer.h new file mode 100644 index 0000000..c1785b5 --- /dev/null +++ b/src/attoflow/node_renderer.h @@ -0,0 +1,188 @@ +#pragma once +#include "atto/graph_builder.h" +#include "atto/node_types2.h" +#include "imgui.h" +#include +#include +#include +#include + +// ─── Style ─── + +struct Editor2Style { + float node_min_width = 80.0f; + float node_height = 40.0f; + float pin_radius = 5.0f; + float pin_spacing = 16.0f; + float node_rounding = 4.0f; + float grid_step = 20.0f; + + float wire_thickness = 2.5f; + float node_border = 1.0f; + float highlight_offset = 2.0f; + float highlight_thickness = 2.0f; + float add_pin_line = 1.5f; + + float pin_hit_radius_mul = 2.5f; + float wire_hit_threshold = 30.0f; + float node_hit_threshold_mul = 6.f; + float dismiss_radius = 20.0f; + float pin_priority_bias = 1e6f; + + ImU32 col_bg = IM_COL32(30, 30, 40, 255); + ImU32 col_grid = IM_COL32(50, 50, 60, 255); + + ImU32 col_node = IM_COL32(50, 55, 75, 220); + ImU32 col_node_sel = IM_COL32(80, 90, 130, 255); + ImU32 col_node_err = IM_COL32(130, 40, 40, 220); + ImU32 col_node_border = IM_COL32(80, 80, 100, 255); + ImU32 col_err_border = IM_COL32(255, 80, 80, 255); + ImU32 col_text = IM_COL32(220, 220, 220, 255); + + ImU32 col_pin_data = IM_COL32(100, 200, 100, 255); + ImU32 col_pin_bang = IM_COL32(255, 200, 80, 255); + ImU32 col_pin_lambda = IM_COL32(180, 130, 255, 255); + ImU32 col_pin_hover = IM_COL32(255, 255, 255, 255); + ImU32 col_add_pin = IM_COL32(120, 120, 140, 180); + ImU32 col_add_pin_fg = IM_COL32(200, 200, 220, 220); + ImU32 col_opt_pin_fg = IM_COL32(30, 30, 40, 255); + + ImU32 col_wire = IM_COL32(200, 200, 100, 200); + ImU32 col_wire_named = IM_COL32(200, 200, 100, 120); + ImU32 col_wire_lambda = IM_COL32(180, 130, 255, 200); + + ImU32 col_label_bg = IM_COL32(30, 30, 40, 200); + ImU32 col_label_text = IM_COL32(180, 220, 255, 255); + + float tooltip_scale = 1.0f; +}; + +extern Editor2Style S; + +// ─── Vector helpers ─── + +inline ImVec2 v2add(ImVec2 a, ImVec2 b) { return {a.x + b.x, a.y + b.y}; } +inline ImVec2 v2sub(ImVec2 a, ImVec2 b) { return {a.x - b.x, a.y - b.y}; } +inline ImVec2 v2mul(ImVec2 a, float s) { return {a.x * s, a.y * s}; } + +// ─── VisualPin: typed pin entry replacing magic sentinel indices ─── + +enum class VisualPinKind { Base, VaArg, AbsentOptional, AddDiamond, Remap }; + +struct VisualPin { + VisualPinKind kind; + FlowArg2Ptr arg; // the actual arg (null for AbsentOptional, AddDiamond) + const PortDesc2* port_desc; // port descriptor (null for remaps) + PortKind2 port_kind; // resolved shape (Data, BangTrigger, Lambda, BangNext) + bool is_optional; // for visual rendering of optional markers +}; + +struct VisualPinMap { + std::vector inputs; // input pins in visual order + std::vector outputs; // output pins in visual order (side-bang excluded for flow) + bool has_side_bang = false; + FlowArg2Ptr side_bang_arg; + const PortDesc2* add_diamond_va_port = nullptr; // non-null if +diamond exists + bool is_flow = false; + + static VisualPinMap build(const FlowNodeBuilderPtr& node, const NodeType2* nt); +}; + +// ─── NodeLayout: computed screen-space layout ─── + +struct NodeLayout { + ImVec2 pos; + float width; + float height; + int num_in; + int num_out; + float zoom; + + ImVec2 input_pin_pos(int i) const { + return {pos.x + (i + 0.5f) * S.pin_spacing * zoom, pos.y}; + } + ImVec2 output_pin_pos(int i) const { + return {pos.x + (i + 0.5f) * S.pin_spacing * zoom, pos.y + height}; + } + ImVec2 lambda_grab_pos() const { + return {pos.x, pos.y + height * 0.5f}; + } + ImVec2 side_bang_pos() const { + return {pos.x + width, pos.y + height * 0.5f}; + } +}; + +NodeLayout compute_node_layout(const FlowNodeBuilderPtr& node, const VisualPinMap& vpm, + ImVec2 canvas_origin, float zoom); + +// ─── WireInfo ─── + +struct WireInfo { + BuilderEntryPtr entry_; + ImVec2 p0, p1, p2, p3; + NodeId src_id, dst_id, net_id; + BuilderEntryPtr entry() const { return entry_; } + bool is_lambda() const { return entry_ && entry_->is(IdCategory::Node); } +}; + +// ─── AddPinHover ─── + +struct AddPinHover { + FlowNodeBuilderPtr node; + const PortDesc2* va_port; + bool is_input; +}; + +// ─── HoverItem ─── + +using HoverItem = std::variant; + +// ─── NodeRenderState: editor-derived state passed to renderer ─── + +struct NodeRenderState { + bool selected; + bool node_hovered; + bool pin_hovered_on_this; + FlowArg2Ptr hovered_pin; // null if no pin hovered + const AddPinHover* add_pin_hover; // null if no +diamond hovered +}; + +// ─── Hit-testing ─── + +struct HitResult { + HoverItem item; + float distance = 1e18f; +}; + +struct NodeHitTarget { + FlowNodeBuilderPtr node; + const NodeType2* nt; + const NodeLayout* layout; + const VisualPinMap* vpm; +}; + +float point_to_bezier_dist(ImVec2 p, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3); +ImU32 pin_color(PortKind2 kind); + +HitResult hit_test_wires(ImVec2 mouse, const std::vector& wires, float zoom); +HitResult hit_test_node_bodies(ImVec2 mouse, const std::vector& nodes, float zoom); +HitResult hit_test_pins(ImVec2 mouse, const std::vector& nodes, float zoom); + +// ─── Rendering functions ─── + +void render_background(ImDrawList* dl, ImVec2 canvas_p0, ImVec2 canvas_sz, + ImVec2 canvas_offset, float zoom); + +void render_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, const NodeType2* nt, + const NodeLayout& layout, const VisualPinMap& vpm, + const std::string& display_text, const NodeRenderState& state, + float zoom, bool draw_tooltips); + +void render_wire(ImDrawList* dl, const WireInfo& w, float zoom); +void render_wire_label(ImDrawList* dl, const WireInfo& w, float zoom); +void render_wire_highlight(ImDrawList* dl, const WireInfo& w, float zoom); +void render_selection_rect(ImDrawList* dl, ImVec2 p0, ImVec2 p1); + +WireInfo compute_wire_geometry(ImVec2 from, ImVec2 to, bool is_lambda, bool is_side_bang, + float zoom, const BuilderEntryPtr& entry, + const NodeId& src_id, const NodeId& dst_id, const NodeId& net_id); From 0f5ca5c613c2e2eab1db6aef273c25f5c20d45e4 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 22:19:51 +0200 Subject: [PATCH 76/86] Extract tooltip rendering functionality and style management for editor nodes --- CMakeLists.txt | 2 + src/attoflow/editor2.cpp | 11 +---- src/attoflow/editor_style.cpp | 53 ++++++++++++++++++++ src/attoflow/editor_style.h | 63 ++++++++++++++++++++++++ src/attoflow/node_renderer.cpp | 76 +++++------------------------ src/attoflow/node_renderer.h | 53 +------------------- src/attoflow/tooltip_renderer.cpp | 80 +++++++++++++++++++++++++++++++ src/attoflow/tooltip_renderer.h | 30 ++++++++++++ 8 files changed, 242 insertions(+), 126 deletions(-) create mode 100644 src/attoflow/editor_style.cpp create mode 100644 src/attoflow/editor_style.h create mode 100644 src/attoflow/tooltip_renderer.cpp create mode 100644 src/attoflow/tooltip_renderer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 51a09f3..ee5d377 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,8 @@ if(ATTOLANG_BUILD_EDITOR) src/attoflow/editor.cpp src/attoflow/editor2.cpp src/attoflow/node_renderer.cpp + src/attoflow/tooltip_renderer.cpp + src/attoflow/editor_style.cpp ) target_include_directories(attoflow PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 9aba831..77f9472 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -1,5 +1,6 @@ #include "editor2.h" #include "node_renderer.h" +#include "tooltip_renderer.h" #include "atto/graph_builder.h" #include "atto/node_types2.h" #include "imgui.h" @@ -471,15 +472,7 @@ void Editor2Pane::draw_hover_effects(ImDrawList* dl, ImVec2 canvas_origin, const if (draw_tooltips_) { for (auto& w : cached_wires_) { if (w.entry() == hover_entry) { - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(S.tooltip_scale); - if (w.is_lambda()) - ImGui::Text("lambda: %s", w.src_id.c_str()); - else - ImGui::Text("net: %s", w.net_id.c_str()); - ImGui::Text("src: %s", w.src_id.c_str()); - ImGui::Text("dst: %s", w.dst_id.c_str()); - ImGui::EndTooltip(); + tooltip_wire(w); break; } } diff --git a/src/attoflow/editor_style.cpp b/src/attoflow/editor_style.cpp new file mode 100644 index 0000000..8fad4bb --- /dev/null +++ b/src/attoflow/editor_style.cpp @@ -0,0 +1,53 @@ +#include "editor_style.h" + +Editor2Style::Editor2Style() + // Layout + : node_min_width(80.0f) + , node_height(40.0f) + , pin_radius(5.0f) + , pin_spacing(16.0f) + , node_rounding(4.0f) + , grid_step(20.0f) + // Thickness + , wire_thickness(2.5f) + , node_border(1.0f) + , highlight_offset(2.0f) + , highlight_thickness(2.0f) + , add_pin_line(1.5f) + // Hit testing + , pin_hit_radius_mul(2.5f) + , wire_hit_threshold(30.0f) + , node_hit_threshold_mul(6.f) + , dismiss_radius(20.0f) + , pin_priority_bias(1e6f) + // Canvas colors + , col_bg(IM_COL32(30, 30, 40, 255)) + , col_grid(IM_COL32(50, 50, 60, 255)) + // Node colors + , col_node(IM_COL32(50, 55, 75, 220)) + , col_node_sel(IM_COL32(80, 90, 130, 255)) + , col_node_err(IM_COL32(130, 40, 40, 220)) + , col_node_border(IM_COL32(80, 80, 100, 255)) + , col_err_border(IM_COL32(255, 80, 80, 255)) + , col_text(IM_COL32(220, 220, 220, 255)) + // Pin colors + , col_pin_data(IM_COL32(100, 200, 100, 255)) + , col_pin_bang(IM_COL32(255, 200, 80, 255)) + , col_pin_lambda(IM_COL32(180, 130, 255, 255)) + , col_pin_hover(IM_COL32(255, 255, 255, 255)) + , col_add_pin(IM_COL32(120, 120, 140, 180)) + , col_add_pin_fg(IM_COL32(200, 200, 220, 220)) + , col_opt_pin_fg(IM_COL32(30, 30, 40, 255)) + // Wire colors + , col_wire(IM_COL32(200, 200, 100, 200)) + , col_wire_named(IM_COL32(200, 200, 100, 120)) + , col_wire_lambda(IM_COL32(180, 130, 255, 200)) + // Net label colors + , col_label_bg(IM_COL32(30, 30, 40, 200)) + , col_label_text(IM_COL32(180, 220, 255, 255)) + // Tooltip + , tooltip_scale(1.0f) +{ +} + +Editor2Style S; diff --git a/src/attoflow/editor_style.h b/src/attoflow/editor_style.h new file mode 100644 index 0000000..5983592 --- /dev/null +++ b/src/attoflow/editor_style.h @@ -0,0 +1,63 @@ +#pragma once +#include "imgui.h" + +struct Editor2Style { + Editor2Style(); + + // Layout + float node_min_width; + float node_height; + float pin_radius; + float pin_spacing; + float node_rounding; + float grid_step; + + // Thickness + float wire_thickness; + float node_border; + float highlight_offset; + float highlight_thickness; + float add_pin_line; + + // Hit testing + float pin_hit_radius_mul; + float wire_hit_threshold; + float node_hit_threshold_mul; + float dismiss_radius; + float pin_priority_bias; + + // Canvas colors + ImU32 col_bg; + ImU32 col_grid; + + // Node colors + ImU32 col_node; + ImU32 col_node_sel; + ImU32 col_node_err; + ImU32 col_node_border; + ImU32 col_err_border; + ImU32 col_text; + + // Pin colors + ImU32 col_pin_data; + ImU32 col_pin_bang; + ImU32 col_pin_lambda; + ImU32 col_pin_hover; + ImU32 col_add_pin; + ImU32 col_add_pin_fg; + ImU32 col_opt_pin_fg; + + // Wire colors + ImU32 col_wire; + ImU32 col_wire_named; + ImU32 col_wire_lambda; + + // Net label colors + ImU32 col_label_bg; + ImU32 col_label_text; + + // Tooltip + float tooltip_scale; +}; + +extern Editor2Style S; diff --git a/src/attoflow/node_renderer.cpp b/src/attoflow/node_renderer.cpp index 7b54fab..6361798 100644 --- a/src/attoflow/node_renderer.cpp +++ b/src/attoflow/node_renderer.cpp @@ -1,11 +1,8 @@ #include "node_renderer.h" +#include "tooltip_renderer.h" #include #include -// ─── Style (global instance) ─── - -Editor2Style S; - // ─── Geometry helpers ─── float point_to_bezier_dist(ImVec2 p, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3) { @@ -310,12 +307,8 @@ void render_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, const NodeType2 for (int i = 0; i < (int)vpm.inputs.size(); i++) { if (vpm.inputs[i].kind == VisualPinKind::AddDiamond) { draw_highlight(layout.input_pin_pos(i), PinShape::Diamond); - if (draw_tooltips) { - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(S.tooltip_scale); - ImGui::Text("add %s", state.add_pin_hover->va_port ? state.add_pin_hover->va_port->name : "arg"); - ImGui::EndTooltip(); - } + if (draw_tooltips) + tooltip_add_diamond(*state.add_pin_hover); return; } } @@ -328,15 +321,8 @@ void render_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, const NodeType2 if (pin.kind == VisualPinKind::AddDiamond || pin.kind == VisualPinKind::AbsentOptional) continue; if (pin.arg == state.hovered_pin) { draw_highlight(layout.input_pin_pos(i), pin_shape_for(pin)); - if (draw_tooltips) { - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(S.tooltip_scale); - if (pin.arg->port()) - ImGui::Text("%s", pin.arg->name().c_str()); - else if (pin.kind == VisualPinKind::Remap) - ImGui::Text("$%d", pin.arg->remap_idx()); - ImGui::EndTooltip(); - } + if (draw_tooltips) + tooltip_input_pin(pin); return; } } @@ -345,63 +331,23 @@ void render_node(ImDrawList* dl, const FlowNodeBuilderPtr& node, const NodeType2 auto& pin = vpm.outputs[i]; if (pin.arg == state.hovered_pin) { draw_highlight(layout.output_pin_pos(i), pin_shape_for(pin)); - if (draw_tooltips) { - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(S.tooltip_scale); - if (pin.arg->port()) - ImGui::Text("%s", pin.arg->name().c_str()); - else - ImGui::Text("out%d", i); - ImGui::EndTooltip(); - } + if (draw_tooltips) + tooltip_output_pin(pin, i); return; } } // Side-bang if (vpm.has_side_bang && vpm.side_bang_arg == state.hovered_pin) { draw_highlight(layout.side_bang_pos(), PinShape::Square); - if (draw_tooltips) { - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(S.tooltip_scale); - ImGui::Text("post_bang"); - ImGui::EndTooltip(); - } + if (draw_tooltips) + tooltip_side_bang(); return; } } // Node body tooltip - if (state.node_hovered && draw_tooltips) { - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(S.tooltip_scale); - ImGui::Text("id: %s", node->id().c_str()); - auto show_args = [](const char* label, const ParsedArgs2* pa) { - if (!pa) return; - ImGui::Text("%s (%d):", label, pa->size()); - for (int i = 0; i < pa->size(); i++) { - auto a = (*pa)[i]; - if (auto n = a->as_net()) - ImGui::Text(" [%d] net: %s", i, n->first().c_str()); - else if (auto e = a->as_expr()) - ImGui::Text(" [%d] expr: %s", i, e->expr().c_str()); - else if (auto s = a->as_string()) - ImGui::Text(" [%d] str: %s", i, s->value().c_str()); - else if (auto v = a->as_number()) - ImGui::Text(" [%d] num: %g", i, v->value()); - } - }; - show_args("parsed_args", node->parsed_args.get()); - if (node->parsed_va_args && !node->parsed_va_args->empty()) - show_args("parsed_va_args", node->parsed_va_args.get()); - if (!node->remaps.empty()) { - ImGui::Text("remaps (%d):", (int)node->remaps.size()); - for (int i = 0; i < (int)node->remaps.size(); i++) { - if (auto n = node->remaps[i]->as_net()) - ImGui::Text(" $%d -> %s", i, n->first().c_str()); - } - } - ImGui::EndTooltip(); - } + if (state.node_hovered && draw_tooltips) + tooltip_node_body(node); } // ─── Wire rendering ─── diff --git a/src/attoflow/node_renderer.h b/src/attoflow/node_renderer.h index c1785b5..18cc91d 100644 --- a/src/attoflow/node_renderer.h +++ b/src/attoflow/node_renderer.h @@ -1,4 +1,5 @@ #pragma once +#include "editor_style.h" #include "atto/graph_builder.h" #include "atto/node_types2.h" #include "imgui.h" @@ -7,58 +8,6 @@ #include #include -// ─── Style ─── - -struct Editor2Style { - float node_min_width = 80.0f; - float node_height = 40.0f; - float pin_radius = 5.0f; - float pin_spacing = 16.0f; - float node_rounding = 4.0f; - float grid_step = 20.0f; - - float wire_thickness = 2.5f; - float node_border = 1.0f; - float highlight_offset = 2.0f; - float highlight_thickness = 2.0f; - float add_pin_line = 1.5f; - - float pin_hit_radius_mul = 2.5f; - float wire_hit_threshold = 30.0f; - float node_hit_threshold_mul = 6.f; - float dismiss_radius = 20.0f; - float pin_priority_bias = 1e6f; - - ImU32 col_bg = IM_COL32(30, 30, 40, 255); - ImU32 col_grid = IM_COL32(50, 50, 60, 255); - - ImU32 col_node = IM_COL32(50, 55, 75, 220); - ImU32 col_node_sel = IM_COL32(80, 90, 130, 255); - ImU32 col_node_err = IM_COL32(130, 40, 40, 220); - ImU32 col_node_border = IM_COL32(80, 80, 100, 255); - ImU32 col_err_border = IM_COL32(255, 80, 80, 255); - ImU32 col_text = IM_COL32(220, 220, 220, 255); - - ImU32 col_pin_data = IM_COL32(100, 200, 100, 255); - ImU32 col_pin_bang = IM_COL32(255, 200, 80, 255); - ImU32 col_pin_lambda = IM_COL32(180, 130, 255, 255); - ImU32 col_pin_hover = IM_COL32(255, 255, 255, 255); - ImU32 col_add_pin = IM_COL32(120, 120, 140, 180); - ImU32 col_add_pin_fg = IM_COL32(200, 200, 220, 220); - ImU32 col_opt_pin_fg = IM_COL32(30, 30, 40, 255); - - ImU32 col_wire = IM_COL32(200, 200, 100, 200); - ImU32 col_wire_named = IM_COL32(200, 200, 100, 120); - ImU32 col_wire_lambda = IM_COL32(180, 130, 255, 200); - - ImU32 col_label_bg = IM_COL32(30, 30, 40, 200); - ImU32 col_label_text = IM_COL32(180, 220, 255, 255); - - float tooltip_scale = 1.0f; -}; - -extern Editor2Style S; - // ─── Vector helpers ─── inline ImVec2 v2add(ImVec2 a, ImVec2 b) { return {a.x + b.x, a.y + b.y}; } diff --git a/src/attoflow/tooltip_renderer.cpp b/src/attoflow/tooltip_renderer.cpp new file mode 100644 index 0000000..27730a1 --- /dev/null +++ b/src/attoflow/tooltip_renderer.cpp @@ -0,0 +1,80 @@ +#include "tooltip_renderer.h" +#include "node_renderer.h" + +void tooltip_add_diamond(const AddPinHover& hover) { + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + ImGui::Text("add %s", hover.va_port ? hover.va_port->name : "arg"); + ImGui::EndTooltip(); +} + +void tooltip_input_pin(const VisualPin& pin) { + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + if (pin.arg->port()) + ImGui::Text("%s", pin.arg->name().c_str()); + else if (pin.kind == VisualPinKind::Remap) + ImGui::Text("$%d", pin.arg->remap_idx()); + ImGui::EndTooltip(); +} + +void tooltip_output_pin(const VisualPin& pin, int visual_index) { + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + if (pin.arg->port()) + ImGui::Text("%s", pin.arg->name().c_str()); + else + ImGui::Text("out%d", visual_index); + ImGui::EndTooltip(); +} + +void tooltip_side_bang() { + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + ImGui::Text("post_bang"); + ImGui::EndTooltip(); +} + +void tooltip_node_body(const FlowNodeBuilderPtr& node) { + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + ImGui::Text("id: %s", node->id().c_str()); + auto show_args = [](const char* label, const ParsedArgs2* pa) { + if (!pa) return; + ImGui::Text("%s (%d):", label, pa->size()); + for (int i = 0; i < pa->size(); i++) { + auto a = (*pa)[i]; + if (auto n = a->as_net()) + ImGui::Text(" [%d] net: %s", i, n->first().c_str()); + else if (auto e = a->as_expr()) + ImGui::Text(" [%d] expr: %s", i, e->expr().c_str()); + else if (auto s = a->as_string()) + ImGui::Text(" [%d] str: %s", i, s->value().c_str()); + else if (auto v = a->as_number()) + ImGui::Text(" [%d] num: %g", i, v->value()); + } + }; + show_args("parsed_args", node->parsed_args.get()); + if (node->parsed_va_args && !node->parsed_va_args->empty()) + show_args("parsed_va_args", node->parsed_va_args.get()); + if (!node->remaps.empty()) { + ImGui::Text("remaps (%d):", (int)node->remaps.size()); + for (int i = 0; i < (int)node->remaps.size(); i++) { + if (auto n = node->remaps[i]->as_net()) + ImGui::Text(" $%d -> %s", i, n->first().c_str()); + } + } + ImGui::EndTooltip(); +} + +void tooltip_wire(const WireInfo& w) { + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(S.tooltip_scale); + if (w.is_lambda()) + ImGui::Text("lambda: %s", w.src_id.c_str()); + else + ImGui::Text("net: %s", w.net_id.c_str()); + ImGui::Text("src: %s", w.src_id.c_str()); + ImGui::Text("dst: %s", w.dst_id.c_str()); + ImGui::EndTooltip(); +} diff --git a/src/attoflow/tooltip_renderer.h b/src/attoflow/tooltip_renderer.h new file mode 100644 index 0000000..146fff3 --- /dev/null +++ b/src/attoflow/tooltip_renderer.h @@ -0,0 +1,30 @@ +#pragma once +#include "editor_style.h" +#include "atto/graph_builder.h" +#include "atto/node_types2.h" +#include "imgui.h" +#include +#include + +struct VisualPin; +enum class VisualPinKind; +struct AddPinHover; +struct WireInfo; + +// Tooltip for a hovered +diamond (add va_arg) pin +void tooltip_add_diamond(const AddPinHover& hover); + +// Tooltip for a hovered input pin +void tooltip_input_pin(const VisualPin& pin); + +// Tooltip for a hovered output pin (visual_index for fallback name) +void tooltip_output_pin(const VisualPin& pin, int visual_index); + +// Tooltip for the side-bang output +void tooltip_side_bang(); + +// Tooltip for the node body (detailed debug info) +void tooltip_node_body(const FlowNodeBuilderPtr& node); + +// Tooltip for a hovered wire/net +void tooltip_wire(const WireInfo& w); From 21e950dbde45c0702b12252c0e9ddd2e100778ef Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 22:49:12 +0200 Subject: [PATCH 77/86] Refactor editor structure to introduce IEditorPane interface - Created IEditorPane interface in editor_pane.h to unify editor pane functionality. - Updated Editor1Pane and Editor2Pane to implement IEditorPane, ensuring consistent method signatures for loading, drawing, and state checks. - Added TabState struct to manage editor pane instances, providing convenience accessors for loaded state and file paths. - Enhanced Editor2Pane to inherit from IEditorPane, aligning it with the new architecture. --- CMakeLists.txt | 1 + src/attoflow/editor.cpp | 2480 +++--------------------------------- src/attoflow/editor.h | 126 +- src/attoflow/editor1.cpp | 2010 +++++++++++++++++++++++++++++ src/attoflow/editor1.h | 135 ++ src/attoflow/editor2.h | 17 +- src/attoflow/editor_pane.h | 15 + src/attoflow/tab.h | 14 + 8 files changed, 2337 insertions(+), 2461 deletions(-) create mode 100644 src/attoflow/editor1.cpp create mode 100644 src/attoflow/editor1.h create mode 100644 src/attoflow/editor_pane.h create mode 100644 src/attoflow/tab.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ee5d377..ab2b2db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,6 +75,7 @@ if(ATTOLANG_BUILD_EDITOR) add_executable(attoflow src/attoflow/main.cpp src/attoflow/editor.cpp + src/attoflow/editor1.cpp src/attoflow/editor2.cpp src/attoflow/node_renderer.cpp src/attoflow/tooltip_renderer.cpp diff --git a/src/attoflow/editor.cpp b/src/attoflow/editor.cpp index 63706a7..17f9b1b 100644 --- a/src/attoflow/editor.cpp +++ b/src/attoflow/editor.cpp @@ -1,16 +1,10 @@ #include "editor.h" #include "atto/args.h" -#include "atto/expr.h" -#include "atto/inference.h" #include "atto/serial.h" -#include "atto/shadow.h" -#include "atto/types.h" #include #include #include -#include #include -#include #include #ifndef _WIN32 #include @@ -21,263 +15,12 @@ #include #endif -static constexpr float NODE_ROUNDING = 4.0f; -static constexpr float PIN_RADIUS = 5.0f; -static constexpr float PIN_SPACING = 20.0f; -static constexpr float NODE_HEIGHT = 31.0f; -static constexpr float NODE_MIN_WIDTH = 80.0f; -static constexpr float GRID_SIZE = 32.0f; - -static constexpr ImU32 COL_BG = IM_COL32(30, 30, 40, 255); -static constexpr ImU32 COL_GRID = IM_COL32(50, 50, 60, 255); -static constexpr ImU32 COL_NODE_BG = IM_COL32(60, 60, 90, 230); -static constexpr ImU32 COL_PIN_IN = IM_COL32(100, 200, 100, 255); -static constexpr ImU32 COL_PIN_OUT = IM_COL32(200, 100, 100, 255); -static constexpr ImU32 COL_PIN_HOVER = IM_COL32(255, 255, 255, 255); -static constexpr ImU32 COL_LINK = IM_COL32(200, 200, 100, 200); -static constexpr ImU32 COL_LINK_DRAG = IM_COL32(255, 255, 150, 200); - -#include "atto/node_types.h" - -// Look up port description for a pin on a node. -// Returns {port_name, port_desc} or {"", ""} if not found. -static std::pair get_port_desc(const FlowNode& node, const FlowPin& pin) { - // Use the pin's own name — it reflects $N:name annotations from parse_args() - // For descriptor pins (non-$N), the name comes from the node type descriptor - // For $N ref pins, the name is either the numeric index or the :name annotation - if (node.lambda_grab.id == pin.id) return {"as_lambda", "pass as lambda"}; - if (node.bang_pin.id == pin.id) return {"bang", "bang connector"}; - - auto* nt = find_node_type(node.type_id); - - // For bang pins, use descriptor names - auto find_bang = [&](const auto& pins, const PortDesc* descs, int count) -> std::pair { - int idx = 0; - for (auto& p : pins) { - if (p->id == pin.id) { - if (descs && idx < count) return {descs[idx].name, descs[idx].desc}; - return {pin.name, ""}; - } - idx++; - } - return {"", ""}; - }; - - if (nt) { - auto r = find_bang(node.triggers, nt->trigger_ports, nt->num_triggers); - if (!r.first.empty()) return r; - r = find_bang(node.nexts, nt->next_ports, nt->num_nexts); - if (!r.first.empty()) return r; - r = find_bang(node.outputs, nt->output_ports, nt->outputs); - if (!r.first.empty()) return r; - } - - // For data input pins: check if a $N:name annotation exists in parsed expressions - for (int i = 0; i < (int)node.inputs.size(); i++) { - if (node.inputs[i]->id != pin.id) continue; - // Look for a PinRef with this index that has a :name annotation - for (auto& expr : node.parsed_exprs) { - if (!expr) continue; - // Walk AST to find PinRef for this pin index - struct Finder { - int target_idx; std::string result; - void walk(const ExprPtr& e) { - if (!e || !result.empty()) return; - if (e->kind == ExprKind::PinRef && e->pin_ref.index == target_idx && !e->pin_ref.name.empty()) - result = e->pin_ref.name; - for (auto& c : e->children) walk(c); - } - }; - // Parse pin name as index - int pin_idx = -1; - try { pin_idx = std::stoi(pin.name); } catch (...) {} - if (pin_idx >= 0) { - Finder f{pin_idx, {}}; - f.walk(expr); - if (!f.result.empty()) return {f.result, ""}; - } - } - return {pin.name, ""}; - } - - return {pin.name, ""}; -} - -// Get a display-friendly name for a node -static std::string node_display_name(const FlowNode& node) { - return node.display_text(); -} - -// Build "display_name.port_name" label for a pin -static std::string pin_label(const FlowNode& node, const FlowPin& pin) { - auto [port_name, _] = get_port_desc(node, pin); - return node_display_name(node) + "." + port_name; -} - -#include "atto/type_utils.h" - -static float dist2(ImVec2 a, ImVec2 b) { - float dx = a.x - b.x, dy = a.y - b.y; - return dx * dx + dy * dy; -} - -static float point_to_bezier_dist(ImVec2 p, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3) { - float min_d2 = 1e18f; - for (int i = 0; i <= 20; i++) { - float t = i / 20.0f; - float u = 1.0f - t; - float x = u*u*u*p0.x + 3*u*u*t*p1.x + 3*u*t*t*p2.x + t*t*t*p3.x; - float y = u*u*u*p0.y + 3*u*u*t*p1.y + 3*u*t*t*p2.y + t*t*t*p3.y; - float dx = p.x - x, dy = p.y - y; - float d2 = dx*dx + dy*dy; - if (d2 < min_d2) min_d2 = d2; - } - return std::sqrt(min_d2); -} - - -enum class PinShape { Square, Signal, LambdaDown, LambdaLeft }; - -static void draw_pin(ImDrawList* dl, ImVec2 pos, float r, ImU32 col, PinShape shape, float zoom) { - switch (shape) { - case PinShape::Signal: - dl->AddCircleFilled(pos, r, col); - { - float font_sz = r * 1.6f; - if (font_sz > 3.0f) { - ImVec2 ts = ImGui::CalcTextSize("~"); - float scale = font_sz / ImGui::GetFontSize(); - dl->AddText(nullptr, font_sz, - {pos.x - ts.x * scale * 0.5f, pos.y - ts.y * scale * 0.5f}, - IM_COL32(30, 30, 40, 255), "~"); - } - } - break; - case PinShape::LambdaDown: - // Down-pointing triangle (for lambda inputs on top) - dl->AddTriangleFilled( - {pos.x - r, pos.y - r}, - {pos.x + r, pos.y - r}, - {pos.x, pos.y + r}, - col); - break; - case PinShape::LambdaLeft: - // Left-pointing triangle (for lambda grab on left) - dl->AddTriangleFilled( - {pos.x + r, pos.y - r}, - {pos.x - r, pos.y}, - {pos.x + r, pos.y + r}, - col); - break; - case PinShape::Square: - default: - dl->AddRectFilled({pos.x - r, pos.y - r}, {pos.x + r, pos.y + r}, col); - break; - } -} - -static void draw_pin_highlight(ImDrawList* dl, ImVec2 pos, float r, ImU32 col, PinShape shape, float zoom) { - float o = 2 * zoom; - switch (shape) { - case PinShape::Signal: - dl->AddCircle(pos, r + o, col, 0, 2.0f); - break; - case PinShape::LambdaDown: - dl->AddTriangle( - {pos.x - r - o, pos.y - r - o}, - {pos.x + r + o, pos.y - r - o}, - {pos.x, pos.y + r + o}, - col, 2.0f); - break; - case PinShape::LambdaLeft: - dl->AddTriangle( - {pos.x + r + o, pos.y - r - o}, - {pos.x - r - o, pos.y}, - {pos.x + r + o, pos.y + r + o}, - col, 2.0f); - break; - case PinShape::Square: - default: - dl->AddRect({pos.x - r - o, pos.y - r - o}, {pos.x + r + o, pos.y + r + o}, col, 0, 0, 2.0f); - break; - } -} - -static void draw_vbezier(ImDrawList* dl, ImVec2 from, ImVec2 to, ImU32 col, float thickness, float zoom) { - float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * zoom); - dl->AddBezierCubic(from, {from.x, from.y + dy}, {to.x, to.y - dy}, to, col, thickness * zoom); -} - -// Sample a cubic bezier at parameter t -static ImVec2 bezier_sample(ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3, float t) { - float u = 1.0f - t; - float uu = u * u, uuu = uu * u; - float tt = t * t, ttt = tt * t; - return {uuu*p0.x + 3*uu*t*p1.x + 3*u*tt*p2.x + ttt*p3.x, - uuu*p0.y + 3*uu*t*p1.y + 3*u*tt*p2.y + ttt*p3.y}; -} - -static void draw_dashed_bezier(ImDrawList* dl, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3, - ImU32 col, float thickness, float dash_len, float gap_len) { - const int N = 128; - // Pre-sample curve points and cumulative arc lengths - ImVec2 pts[N + 1]; - float arc[N + 1]; - pts[0] = p0; arc[0] = 0; - for (int i = 1; i <= N; i++) { - pts[i] = bezier_sample(p0, p1, p2, p3, (float)i / N); - float dx = pts[i].x - pts[i-1].x, dy = pts[i].y - pts[i-1].y; - arc[i] = arc[i-1] + sqrtf(dx*dx + dy*dy); - } - float total = arc[N]; - if (total < 1.0f) return; - float cycle = dash_len + gap_len; - - // Interpolate a point at a given arc distance - auto lerp_at = [&](float d) -> ImVec2 { - if (d <= 0) return pts[0]; - if (d >= total) return pts[N]; - // Binary search for segment - int lo = 0, hi = N; - while (lo < hi - 1) { int mid = (lo+hi)/2; if (arc[mid] < d) lo = mid; else hi = mid; } - float seg_len = arc[hi] - arc[lo]; - float t = (seg_len > 0) ? (d - arc[lo]) / seg_len : 0; - return {pts[lo].x + t * (pts[hi].x - pts[lo].x), - pts[lo].y + t * (pts[hi].y - pts[lo].y)}; - }; - - // Draw dashes - float d = 0; - while (d < total) { - float d_end = std::min(d + dash_len, total); - // Draw the dash as a series of short line segments - ImVec2 prev = lerp_at(d); - float step = 3.0f; // pixels per sub-segment - for (float dd = d + step; dd <= d_end; dd += step) { - ImVec2 cur = lerp_at(dd); - dl->AddLine(prev, cur, col, thickness); - prev = cur; - } - // Final segment to exact end - ImVec2 end = lerp_at(d_end); - dl->AddLine(prev, end, col, thickness); - d += cycle; - } -} - -static void draw_dashed_vbezier(ImDrawList* dl, ImVec2 from, ImVec2 to, ImU32 col, float thickness, float zoom) { - float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * zoom); - draw_dashed_bezier(dl, from, {from.x, from.y + dy}, {to.x, to.y - dy}, to, - col, thickness * zoom, 8.0f * zoom, 4.0f * zoom); -} - bool FlowEditorWindow::init(const std::string& project_dir) { if (!win_.init("Flow Editor", 900, 600)) return false; project_dir_ = project_dir; if (!project_dir_.empty()) { scan_project_files(); - // Open main.atto as the first tab namespace fs = std::filesystem; std::string main_path = (fs::path(project_dir_) / "main.atto").string(); if (fs::exists(main_path)) { @@ -287,10 +30,10 @@ bool FlowEditorWindow::init(const std::string& project_dir) { } } - // Ensure at least one tab exists if (tabs_.empty()) { - tabs_.push_back({}); - tabs_.back().tab_name = "untitled"; + TabState tab; + tab.pane = std::make_shared(); + tabs_.push_back(std::move(tab)); } return true; @@ -314,111 +57,51 @@ void FlowEditorWindow::open_tab(const std::string& file_path) { // Check if already open for (int i = 0; i < (int)tabs_.size(); i++) { - if (tabs_[i].file_path == abs_path) { + if (tabs_[i].pane && tabs_[i].file_path() == abs_path) { active_tab_ = i; return; } } - // Create new tab - TabState tab; - tab.file_path = abs_path; - tab.tab_name = fs::path(file_path).stem().string(); - tab.use_editor2 = true; - tab.editor2 = std::make_shared(); + // Try Editor2Pane first + auto editor2 = std::make_shared(); + std::shared_ptr pane; if (fs::exists(abs_path)) { - // Try loading via Editor2Pane first - if (!tab.editor2->load(abs_path)) { - // Fallback to legacy loader - tab.use_editor2 = false; - load_atto(abs_path, tab.graph); + if (editor2->load(abs_path)) { + pane = editor2; + } else { + // Fallback to legacy Editor1Pane + auto editor1 = std::make_shared(); + editor1->load(abs_path); + pane = editor1; } + } else { + pane = editor2; } - if (!tab.use_editor2 && tab.graph.has_viewport) { - tab.canvas_offset = {tab.graph.viewport_x, tab.graph.viewport_y}; - tab.canvas_zoom = tab.graph.viewport_zoom; - } - tab.inference_dirty = true; + TabState tab; + tab.pane = pane; tabs_.push_back(std::move(tab)); active_tab_ = (int)tabs_.size() - 1; } void FlowEditorWindow::close_tab(int idx) { if (idx < 0 || idx >= (int)tabs_.size()) return; - // Auto-save before closing - if (tabs_[idx].dirty && !tabs_[idx].file_path.empty()) { - sync_viewport(tabs_[idx]); - save_atto(tabs_[idx].file_path, tabs_[idx].graph); + // Auto-save before closing (Editor1Pane handles its own save) + if (auto e1 = std::dynamic_pointer_cast(tabs_[idx].pane)) { + if (e1->is_dirty() && !e1->file_path().empty()) { + e1->sync_viewport(); + e1->auto_save(); + } } tabs_.erase(tabs_.begin() + idx); if (active_tab_ >= (int)tabs_.size()) active_tab_ = std::max(0, (int)tabs_.size() - 1); - // Ensure at least one tab if (tabs_.empty()) { - tabs_.push_back({}); - tabs_.back().tab_name = "untitled"; - } -} - -void FlowEditorWindow::mark_dirty() { - push_undo(); - active().dirty = true; - active().inference_dirty = true; - schedule_save(); -} - -void FlowEditorWindow::push_undo() { - active().undo_stack.push_back(save_atto_string(active().graph)); - active().redo_stack.clear(); - // Limit undo history - if (active().undo_stack.size() > 200) active().undo_stack.erase(active().undo_stack.begin()); -} - -void FlowEditorWindow::undo() { - if (active().undo_stack.empty()) return; - // Save current state to redo - active().redo_stack.push_back(save_atto_string(active().graph)); - // Restore from undo - load_atto_string(active().undo_stack.back(), active().graph); - active().undo_stack.pop_back(); - active().dirty = true; -} - -void FlowEditorWindow::redo() { - if (active().redo_stack.empty()) return; - // Save current state to undo (without clearing redo) - active().undo_stack.push_back(save_atto_string(active().graph)); - // Restore from redo - load_atto_string(active().redo_stack.back(), active().graph); - active().redo_stack.pop_back(); - active().dirty = true; -} - -void FlowEditorWindow::schedule_save() { - active().dirty = true; - save_deadline_ = ImGui::GetTime() + 0.5; // 500ms debounce -} - -void FlowEditorWindow::check_debounced_save() { - if (save_deadline_ > 0 && ImGui::GetTime() >= save_deadline_) { - save_deadline_ = 0; - auto_save(); - } -} - -void FlowEditorWindow::sync_viewport(TabState& tab) { - tab.graph.viewport_x = tab.canvas_offset.x; - tab.graph.viewport_y = tab.canvas_offset.y; - tab.graph.viewport_zoom = tab.canvas_zoom; -} - -void FlowEditorWindow::auto_save() { - if (active().dirty && !active().file_path.empty()) { - sync_viewport(active()); - save_atto(active().file_path, active().graph); - active().dirty = false; + TabState tab; + tab.pane = std::make_shared(); + tabs_.push_back(std::move(tab)); } } @@ -428,356 +111,8 @@ void FlowEditorWindow::shutdown() { build_thread_.join(); win_.shutdown(); } -void FlowEditorWindow::process_event(SDL_Event& e) { win_.process_event(e); } - -ImVec2 FlowEditorWindow::canvas_to_screen(ImVec2 p, ImVec2 origin) const { - return {origin.x + (p.x + active().canvas_offset.x) * active().canvas_zoom, - origin.y + (p.y + active().canvas_offset.y) * active().canvas_zoom}; -} - -ImVec2 FlowEditorWindow::screen_to_canvas(ImVec2 p, ImVec2 origin) const { - return {(p.x - origin.x) / active().canvas_zoom - active().canvas_offset.x, - (p.y - origin.y) / active().canvas_zoom - active().canvas_offset.y}; -} - -ImVec2 FlowEditorWindow::get_pin_pos(const FlowNode& node, const FlowPin& pin, ImVec2 origin) const { - if (pin.direction == FlowPin::LambdaGrab) { - // Grab handle: middle-left - float x = node.position.x; - float y = node.position.y + node.size.y * 0.5f; - return canvas_to_screen({x, y}, origin); - } - - // Bang pin: middle-right - if (pin.id == node.bang_pin.id && pin.name == "bang") { - float x = node.position.x + node.size.x; - float y = node.position.y + node.size.y * 0.5f; - return canvas_to_screen({x, y}, origin); - } - - // Bang inputs first on top - if (pin.direction == FlowPin::BangTrigger) { - int idx = 0; - for (auto& p : node.triggers) { if (p->id == pin.id) break; idx++; } - float x = node.position.x + PIN_SPACING * (idx + 0.5f); - float y = node.position.y; - return canvas_to_screen({x, y}, origin); - } - - if (pin.direction == FlowPin::Input || pin.direction == FlowPin::Lambda) { - // Data inputs and lambdas after bang inputs on the top row. - // Skip shadow-connected pins in slot calculation. - int bang_offset = (int)node.triggers.size(); - int slot = 0; - for (auto& p : node.inputs) { - if (p->id == pin.id) break; - if (!shadow_connected_pins_.count(p->id)) slot++; - } - float x = node.position.x + PIN_SPACING * (bang_offset + slot + 0.5f); - float y = node.position.y; - return canvas_to_screen({x, y}, origin); - } - - // Bang outputs first on bottom, then data outputs - if (pin.direction == FlowPin::BangNext) { - int idx = 0; - for (auto& p : node.nexts) { if (p->id == pin.id) break; idx++; } - float x = node.position.x + PIN_SPACING * (idx + 0.5f); - float y = node.position.y + node.size.y; - return canvas_to_screen({x, y}, origin); - } - - // Data outputs after bang outputs - int offset = (int)node.nexts.size(); - int idx = 0; - for (auto& p : node.outputs) { if (p->id == pin.id) break; idx++; } - float x = node.position.x + PIN_SPACING * (offset + idx + 0.5f); - float y = node.position.y + node.size.y; - return canvas_to_screen({x, y}, origin); -} - -FlowEditorWindow::PinHit FlowEditorWindow::hit_test_pin(ImVec2 sp, ImVec2 co, float radius) const { - float r2 = radius * radius * active().canvas_zoom * active().canvas_zoom; - for (auto& node : active().graph.nodes) { - if (node.imported || node.shadow) continue; - for (auto& pin : node.triggers) - if (dist2(sp, get_pin_pos(node, *pin, co)) < r2) - return {node.id, pin->id, FlowPin::BangTrigger}; - for (auto& pin : node.inputs) - if (dist2(sp, get_pin_pos(node, *pin, co)) < r2) - return {node.id, pin->id, pin->direction}; - for (auto& pin : node.outputs) - if (dist2(sp, get_pin_pos(node, *pin, co)) < r2) - return {node.id, pin->id, FlowPin::Output}; - for (auto& pin : node.nexts) - if (dist2(sp, get_pin_pos(node, *pin, co)) < r2) - return {node.id, pin->id, FlowPin::BangNext}; - if (!node.lambda_grab.id.empty() && dist2(sp, get_pin_pos(node, node.lambda_grab, co)) < r2) { - auto* nt_hit = find_node_type(node.type_id); - if (nt_hit && nt_hit->has_lambda) - return {node.id, node.lambda_grab.id, FlowPin::LambdaGrab}; - } - if (!node.bang_pin.id.empty() && dist2(sp, get_pin_pos(node, node.bang_pin, co)) < r2) { - auto* nt_hit = find_node_type(node.type_id); - bool hidden = (nt_hit && (nt_hit->is_event || nt_hit->no_post_bang)); - if (!hidden) return {node.id, node.bang_pin.id, FlowPin::BangNext}; - } - } - return {-1, "", FlowPin::Input}; -} - -int FlowEditorWindow::hit_test_link(ImVec2 sp, ImVec2 co, float threshold) const { - for (auto& link : active().graph.links) { - ImVec2 fp = {}, tp = {}; - bool ff = false, ft = false; - bool from_grab = false, from_bang_pin = false, to_lambda = false; - for (auto& n : active().graph.nodes) { - for (auto& p : n.outputs) if (p->id == link.from_pin) { fp = get_pin_pos(n, *p, co); ff = true; } - for (auto& p : n.nexts) if (p->id == link.from_pin) { fp = get_pin_pos(n, *p, co); ff = true; } - for (auto& p : n.triggers) if (p->id == link.from_pin) { fp = get_pin_pos(n, *p, co); ff = true; } - if (n.lambda_grab.id == link.from_pin) { fp = get_pin_pos(n, n.lambda_grab, co); ff = true; from_grab = true; } - for (auto& p : n.triggers) if (p->id == link.to_pin) { tp = get_pin_pos(n, *p, co); ft = true; } - for (auto& p : n.inputs) if (p->id == link.to_pin) { tp = get_pin_pos(n, *p, co); ft = true; if (p->direction == FlowPin::Lambda) to_lambda = true; } - if (n.bang_pin.id == link.from_pin) { fp = get_pin_pos(n, n.bang_pin, co); ff = true; from_bang_pin = true; } - } - if (!ff || !ft) continue; - // Use the same curve shape as draw_link for accurate hit testing - float d; - if (from_grab) { - float dx = std::max(std::abs(tp.x - fp.x) * 0.5f, 30.0f * active().canvas_zoom); - float dy = std::max(std::abs(tp.y - fp.y) * 0.5f, 30.0f * active().canvas_zoom); - d = point_to_bezier_dist(sp, fp, {fp.x - dx, fp.y}, {tp.x, tp.y - dy}, tp); - } else if (from_bang_pin) { - float dx = std::max(std::abs(tp.x - fp.x) * 0.5f, 30.0f * active().canvas_zoom); - float dy_hit = std::max(std::abs(tp.y - fp.y) * 0.5f, 30.0f * active().canvas_zoom); - d = point_to_bezier_dist(sp, fp, {fp.x + dx, fp.y}, {tp.x, tp.y - dy_hit}, tp); - } else { - float dy = std::max(std::abs(tp.y - fp.y) * 0.5f, 30.0f * active().canvas_zoom); - d = point_to_bezier_dist(sp, fp, {fp.x, fp.y + dy}, {tp.x, tp.y - dy}, tp); - } - if (d < threshold * active().canvas_zoom) return link.id; - } - return -1; -} - -void FlowEditorWindow::draw_node(ImDrawList* dl, FlowNode& node, ImVec2 origin) { - bool is_label = (node.type_id == NodeTypeID::Label); - - // Width from pins (top row = inputs + lambdas, bottom row = outputs) - int visible_inputs = 0; - for (auto& pin : node.inputs) - if (!shadow_connected_pins_.count(pin->id)) visible_inputs++; - int top_pins = (int)node.triggers.size() + visible_inputs; - int bottom_pins = (int)(node.nexts.size() + node.outputs.size()); - int max_pins = std::max(top_pins, bottom_pins); - float pin_w = (float)(max_pins + 1) * PIN_SPACING; - - // Width from display text - std::string display_text; - if (is_label) { - display_text = node.args.empty() ? "(label)" : node.args; - } else { - display_text = node.display_text(); - } - float font_scale = 17.0f / ImGui::GetFontSize(); - ImVec2 ts = ImGui::CalcTextSize(display_text.c_str()); - float text_w = ts.x * font_scale + 16.0f; // padding - - float needed_w = std::max({pin_w, text_w, NODE_MIN_WIDTH}); - node.size = {needed_w, NODE_HEIGHT}; - - ImVec2 tl = canvas_to_screen(to_imvec(node.position), origin); - ImVec2 br = canvas_to_screen({node.position.x + node.size.x, - node.position.y + node.size.y}, origin); - - if (is_label) { - // Labels: no background box, just text - float font_size = 17.0f * active().canvas_zoom; - if (font_size > 6.0f && editing_node_ != node.id) { - const char* display = node.args.empty() ? "(label)" : node.args.c_str(); - ImU32 col = node.args.empty() ? IM_COL32(100, 100, 100, 180) : IM_COL32(255, 255, 255, 255); - dl->AddText(nullptr, font_size, - {tl.x + 2 * active().canvas_zoom, tl.y + (br.y - tl.y - font_size) * 0.5f}, - col, display); - } - } else { - // Normal node: filled bar (red if error) - ImU32 bg = node.error.empty() ? COL_NODE_BG : IM_COL32(120, 30, 30, 230); - ImU32 border = node.error.empty() ? IM_COL32(100, 100, 150, 255) : IM_COL32(200, 60, 60, 255); - dl->AddRectFilled(tl, br, bg, NODE_ROUNDING * active().canvas_zoom); - - // Highlight animation: blink dark yellow overlay - if (active().highlight_node_id == node.id && active().highlight_timer > 0.0f) { - float blink = std::sin(active().highlight_timer * 6.0f) * 0.5f + 0.5f; - int a = (int)(blink * 140.0f); - dl->AddRectFilled(tl, br, IM_COL32(180, 160, 40, a), NODE_ROUNDING * active().canvas_zoom); - } - - dl->AddRect(tl, br, border, NODE_ROUNDING * active().canvas_zoom); - - // Selection highlight - if (active().selected_nodes.count(node.id)) { - dl->AddRect({tl.x - 2*active().canvas_zoom, tl.y - 2*active().canvas_zoom}, - {br.x + 2*active().canvas_zoom, br.y + 2*active().canvas_zoom}, - IM_COL32(100, 180, 255, 200), NODE_ROUNDING * active().canvas_zoom, 0, 2.0f * active().canvas_zoom); - } - - // Display text - float font_size = 17.0f * active().canvas_zoom; - if (font_size > 6.0f && editing_node_ != node.id) { - std::string text = node.display_text(); - float scale = font_size / ImGui::GetFontSize(); - ImVec2 text_sz = ImGui::CalcTextSize(text.c_str()); - float tw = text_sz.x * scale; - float cx = (tl.x + br.x) * 0.5f - tw * 0.5f; - float cy = tl.y + (br.y - tl.y - font_size) * 0.5f; - dl->AddText(nullptr, font_size, {cx, cy}, IM_COL32(220, 220, 220, 255), text.c_str()); - } - } - - auto* nt = find_node_type(node.type_id); - bool is_event = nt && nt->is_event; - - // Pins - PinShape io_shape = PinShape::Signal; - float pr = PIN_RADIUS * active().canvas_zoom; - { - // Bang inputs (top, before data inputs) - for (auto& pin : node.triggers) { - ImVec2 pp = get_pin_pos(node, *pin, origin); - draw_pin(dl, pp, pr, IM_COL32(255, 200, 80, 255), PinShape::Square, active().canvas_zoom); - } - for (auto& pin : node.inputs) { - if (shadow_connected_pins_.count(pin->id)) continue; - ImVec2 pp = get_pin_pos(node, *pin, origin); - if (pin->direction == FlowPin::Lambda) - draw_pin(dl, pp, pr, IM_COL32(180, 130, 255, 255), PinShape::LambdaDown, active().canvas_zoom); - else - draw_pin(dl, pp, pr, COL_PIN_IN, io_shape, active().canvas_zoom); - } - // Bang outputs (bottom, before data outputs) - for (auto& pin : node.nexts) { - ImVec2 pp = get_pin_pos(node, *pin, origin); - draw_pin(dl, pp, pr, IM_COL32(255, 200, 80, 255), PinShape::Square, active().canvas_zoom); - } - for (auto& pin : node.outputs) { - ImVec2 pp = get_pin_pos(node, *pin, origin); - draw_pin(dl, pp, pr, COL_PIN_OUT, io_shape, active().canvas_zoom); - } - // Lambda grab handle (left) — not on event nodes - bool show_lambda = nt && nt->has_lambda; - if (!node.lambda_grab.id.empty() && show_lambda) { - ImVec2 pp = get_pin_pos(node, node.lambda_grab, origin); - draw_pin(dl, pp, pr, IM_COL32(180, 130, 255, 150), PinShape::LambdaLeft, active().canvas_zoom); - } - // Bang pin (right) — not on event nodes or no_post_bang nodes - bool no_post_bang = nt && nt->no_post_bang; - if (!node.bang_pin.id.empty() && !is_event && !no_post_bang) { - ImVec2 pp = get_pin_pos(node, node.bang_pin, origin); - draw_pin(dl, pp, pr * 0.7f, IM_COL32(255, 200, 80, 255), PinShape::Square, active().canvas_zoom); - } - } -} -void FlowEditorWindow::draw_link(ImDrawList* dl, const FlowLink& link, ImVec2 origin) { - ImVec2 fp = {}, tp = {}; - bool ff = false, ft = false; - bool to_lambda = false; - bool from_grab = false; - bool from_bang_pin = false; - FlowPin* from_pin_ptr = nullptr; - FlowPin* to_pin_ptr = nullptr; - for (auto& n : active().graph.nodes) { - for (auto& p : n.outputs) if (p->id == link.from_pin) { fp = get_pin_pos(n, *p, origin); ff = true; from_pin_ptr = p.get(); } - for (auto& p : n.nexts) if (p->id == link.from_pin) { fp = get_pin_pos(n, *p, origin); ff = true; from_pin_ptr = p.get(); } - for (auto& p : n.triggers) if (p->id == link.from_pin) { fp = get_pin_pos(n, *p, origin); ff = true; from_pin_ptr = p.get(); } - if (n.lambda_grab.id == link.from_pin) { fp = get_pin_pos(n, n.lambda_grab, origin); ff = true; from_grab = true; from_pin_ptr = &n.lambda_grab; } - if (n.bang_pin.id == link.from_pin) { fp = get_pin_pos(n, n.bang_pin, origin); ff = true; from_bang_pin = true; from_pin_ptr = &n.bang_pin; } - for (auto& p : n.triggers) if (p->id == link.to_pin) { tp = get_pin_pos(n, *p, origin); ft = true; to_pin_ptr = p.get(); } - for (auto& p : n.inputs) if (p->id == link.to_pin) { tp = get_pin_pos(n, *p, origin); ft = true; to_pin_ptr = p.get(); if (p->direction == FlowPin::Lambda) to_lambda = true; } - } - if (!ff || !ft) return; - - // Check type compatibility for link coloring - bool type_error = !link.error.empty(); // lambda/inference errors - if (!type_error && from_pin_ptr && to_pin_ptr && - from_pin_ptr->resolved_type && to_pin_ptr->resolved_type && - !from_pin_ptr->resolved_type->is_generic && !to_pin_ptr->resolved_type->is_generic) { - type_error = !types_compatible(from_pin_ptr->resolved_type, to_pin_ptr->resolved_type); - } - - // Check if from-pin is a trigger (top of node, bidirectional) - bool from_trigger = from_pin_ptr && from_pin_ptr->direction == FlowPin::BangTrigger; - - ImU32 col_error = IM_COL32(255, 60, 60, 220); - bool named = !link.net_name.empty() && !link.auto_wire; - - // Dim named wires so the label stands out more - auto dim = [](ImU32 c) -> ImU32 { - return (c & 0x00FFFFFF) | (((c >> 24) * 100 / 255) << 24); - }; - if (named) col_error = dim(col_error); - - auto wire_col = [&](ImU32 c) { return named ? dim(c) : c; }; - - if (from_trigger) { - float dy = std::max(std::abs(tp.y - fp.y) * 0.5f, 40.0f * active().canvas_zoom); - ImU32 col = type_error ? col_error : wire_col(IM_COL32(255, 200, 80, 200)); - float th = 2.5f * active().canvas_zoom; - if (named) - draw_dashed_bezier(dl, fp, {fp.x, fp.y - dy}, {tp.x, tp.y - dy}, tp, col, th, 8.0f * active().canvas_zoom, 4.0f * active().canvas_zoom); - else - dl->AddBezierCubic(fp, {fp.x, fp.y - dy}, {tp.x, tp.y - dy}, tp, col, th); - } else if (from_grab) { - float dx = std::max(std::abs(tp.x - fp.x) * 0.5f, 30.0f * active().canvas_zoom); - float dy = std::max(std::abs(tp.y - fp.y) * 0.5f, 30.0f * active().canvas_zoom); - ImU32 col = type_error ? col_error : wire_col(IM_COL32(180, 130, 255, 200)); - float th = 2.5f * active().canvas_zoom; - if (named) - draw_dashed_bezier(dl, fp, {fp.x - dx, fp.y}, {tp.x, tp.y - dy}, tp, col, th, 8.0f * active().canvas_zoom, 4.0f * active().canvas_zoom); - else - dl->AddBezierCubic(fp, {fp.x - dx, fp.y}, {tp.x, tp.y - dy}, tp, col, th); - } else if (from_bang_pin) { - float dx = std::max(std::abs(tp.x - fp.x) * 0.5f, 30.0f * active().canvas_zoom); - float dy = std::max(std::abs(tp.y - fp.y) * 0.5f, 30.0f * active().canvas_zoom); - ImU32 col = type_error ? col_error : wire_col(IM_COL32(255, 200, 80, 200)); - float th = 2.5f * active().canvas_zoom; - if (named) - draw_dashed_bezier(dl, fp, {fp.x + dx, fp.y}, {tp.x, tp.y - dy}, tp, col, th, 8.0f * active().canvas_zoom, 4.0f * active().canvas_zoom); - else - dl->AddBezierCubic(fp, {fp.x + dx, fp.y}, {tp.x, tp.y - dy}, tp, col, th); - } else if (to_lambda) { - ImU32 col = type_error ? col_error : wire_col(IM_COL32(180, 130, 255, 200)); - if (named) - draw_dashed_vbezier(dl, fp, tp, col, 2.5f, active().canvas_zoom); - else - draw_vbezier(dl, fp, tp, col, 2.5f, active().canvas_zoom); - } else { - ImU32 col = type_error ? col_error : wire_col(COL_LINK); - if (named) - draw_dashed_vbezier(dl, fp, tp, col, 2.5f, active().canvas_zoom); - else - draw_vbezier(dl, fp, tp, col, 2.5f, active().canvas_zoom); - } - - // Draw net name label at midpoint if the wire has a user-assigned name - if (!link.net_name.empty() && !link.auto_wire) { - float font_size = ImGui::GetFontSize() * active().canvas_zoom * 0.8f; - if (font_size > 5.0f) { - // Compute midpoint of the bezier (approximate with lerp) - ImVec2 mid = {(fp.x + tp.x) * 0.5f, (fp.y + tp.y) * 0.5f}; - ImVec2 text_sz = ImGui::CalcTextSize(link.net_name.c_str()); - float tw = text_sz.x * (font_size / ImGui::GetFontSize()); - float th = text_sz.y * (font_size / ImGui::GetFontSize()); - float cx = mid.x - tw * 0.5f; - float cy = mid.y - th * 0.5f; - // Background pill - dl->AddRectFilled({cx - 3, cy - 1}, {cx + tw + 3, cy + th + 1}, - IM_COL32(30, 30, 40, 200), 3.0f); - dl->AddText(nullptr, font_size, {cx, cy}, IM_COL32(180, 220, 255, 255), link.net_name.c_str()); - } - } -} +void FlowEditorWindow::process_event(SDL_Event& e) { win_.process_event(e); } void FlowEditorWindow::draw() { if (!win_.open) return; @@ -785,21 +120,6 @@ void FlowEditorWindow::draw() { win_.begin_frame(); ImGui::SetCurrentContext(win_.imgui_ctx); - // Tick highlight timer - if (active().highlight_timer > 0.0f) { - active().highlight_timer -= ImGui::GetIO().DeltaTime; - if (active().highlight_timer <= 0.0f) { - active().highlight_timer = 0.0f; - active().highlight_node_id = -1; - } - } - - // Validate only when graph structure changes - if (active().graph.dirty) { - validate_nodes(); - active().graph.dirty = false; - } - ImGui::SetNextWindowPos({0, 0}); int w, h; SDL_GetWindowSize(win_.window, &w, &h); @@ -810,17 +130,14 @@ void FlowEditorWindow::draw() { ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoBringToFrontOnFocus); - // Toolbar draw_toolbar(); ImGui::Separator(); - // Poll child process poll_child_process(); float total_w = (float)w; float total_h = ImGui::GetContentRegionAvail().y; - // Clamp panel sizes file_panel_width_ = std::clamp(file_panel_width_, 80.0f, total_w * 0.3f); side_panel_width_ = std::clamp(side_panel_width_, 100.0f, total_w * 0.5f); bottom_panel_height_ = std::clamp(bottom_panel_height_, 40.0f, total_h * 0.5f); @@ -835,7 +152,7 @@ void FlowEditorWindow::draw() { namespace fs = std::filesystem; std::string stem = fs::path(fname).stem().string(); bool is_active = (active_tab_ < (int)tabs_.size() && - tabs_[active_tab_].tab_name == stem); + tabs_[active_tab_].tab_name() == stem); if (is_active) ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(100, 200, 255, 255)); if (ImGui::Selectable(stem.c_str(), is_active)) { std::string full_path = (fs::path(project_dir_) / fname).string(); @@ -846,7 +163,6 @@ void FlowEditorWindow::draw() { ImGui::EndChild(); ImGui::EndChild(); - // File browser splitter ImGui::SameLine(); ImGui::Button("##file_vsplitter", {4.0f, total_h}); if (ImGui::IsItemActive()) @@ -862,31 +178,21 @@ void FlowEditorWindow::draw() { // --- Tab bar --- if (ImGui::BeginTabBar("##atto_tabs")) { for (int i = 0; i < (int)tabs_.size(); i++) { - std::string label = tabs_[i].use_editor2 - ? tabs_[i].editor2->tab_name() - : tabs_[i].tab_name; - bool tab_dirty = tabs_[i].use_editor2 - ? tabs_[i].editor2->is_dirty() - : tabs_[i].dirty; - if (tab_dirty) label += "*"; + std::string label = tabs_[i].tab_name(); + if (tabs_[i].is_dirty()) label += "*"; label += "###tab" + std::to_string(i); bool open = true; ImGuiTabItemFlags flags = (i == active_tab_) ? ImGuiTabItemFlags_SetSelected : 0; if (ImGui::BeginTabItem(label.c_str(), &open, flags)) { if (active_tab_ != i) { active_tab_ = i; - // Reset interaction state when switching tabs - editing_node_ = -1; - dragging_node_ = -1; - dragging_link_from_pin_.clear(); - grabbed_links_.clear(); } ImGui::EndTabItem(); } if (!open) { close_tab(i); if (i <= active_tab_ && active_tab_ > 0) active_tab_--; - i--; // re-check this index + i--; } } ImGui::EndTabBar(); @@ -899,1185 +205,61 @@ void FlowEditorWindow::draw() { // --- Canvas --- ImGui::BeginChild("##flow_canvas", {canvas_w, canvas_h}, false, ImGuiWindowFlags_NoScrollbar); - - if (active().use_editor2) { - active().editor2->draw(); - ImGui::EndChild(); // flow_canvas - } else { - // === Legacy Editor1 canvas === - - ImVec2 canvas_origin = ImGui::GetCursorScreenPos(); - ImVec2 canvas_size = ImGui::GetContentRegionAvail(); - ImDrawList* dl = ImGui::GetWindowDrawList(); - - // Background - dl->AddRectFilled(canvas_origin, - {canvas_origin.x + canvas_size.x, canvas_origin.y + canvas_size.y}, COL_BG); - - // Safety: remove any empty-named nodes that aren't currently being edited - // (type validation relaxed — any name is allowed) - std::erase_if(active().graph.nodes, [&](auto& n) { - if (n.id == editing_node_) return false; - if (n.guid.empty()) return true; - return false; - }); - - // Grid - float grid = GRID_SIZE * active().canvas_zoom; - if (grid > 4.0f) { - float ox = std::fmod(active().canvas_offset.x * active().canvas_zoom, grid); - float oy = std::fmod(active().canvas_offset.y * active().canvas_zoom, grid); - for (float x = ox; x < canvas_size.x; x += grid) - dl->AddLine({canvas_origin.x + x, canvas_origin.y}, - {canvas_origin.x + x, canvas_origin.y + canvas_size.y}, COL_GRID); - for (float y = oy; y < canvas_size.y; y += grid) - dl->AddLine({canvas_origin.x, canvas_origin.y + y}, - {canvas_origin.x + canvas_size.x, canvas_origin.y + y}, COL_GRID); - } - - ImGui::InvisibleButton("##canvas", canvas_size, - ImGuiButtonFlags_MouseButtonLeft | - ImGuiButtonFlags_MouseButtonMiddle | - ImGuiButtonFlags_MouseButtonRight); - bool canvas_hovered = ImGui::IsItemHovered(); - ImVec2 mouse_pos = ImGui::GetMousePos(); - - // --- Canvas pan --- - if (canvas_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) { - canvas_dragging_ = true; - canvas_drag_start_ = mouse_pos; - } - if (canvas_dragging_) { - if (ImGui::IsMouseDown(ImGuiMouseButton_Middle)) { - ImVec2 delta = {mouse_pos.x - canvas_drag_start_.x, mouse_pos.y - canvas_drag_start_.y}; - active().canvas_offset.x += delta.x / active().canvas_zoom; - active().canvas_offset.y += delta.y / active().canvas_zoom; - canvas_drag_start_ = mouse_pos; - schedule_save(); - } else { canvas_dragging_ = false; } - } - - // --- Canvas zoom --- - if (canvas_hovered) { - float wheel = ImGui::GetIO().MouseWheel; - if (std::abs(wheel) > 0.01f) { - float zf = std::pow(1.1f, wheel); - ImVec2 mc = screen_to_canvas(mouse_pos, canvas_origin); - active().canvas_zoom *= zf; - active().canvas_zoom = std::clamp(active().canvas_zoom, 0.2f, 5.0f); - ImVec2 mc2 = screen_to_canvas(mouse_pos, canvas_origin); - active().canvas_offset.x += mc2.x - mc.x; - active().canvas_offset.y += mc2.y - mc.y; - schedule_save(); - } - } - - // Helper: hit test node at canvas pos - auto hit_test_node = [&](ImVec2 mc) -> int { - for (int i = (int)active().graph.nodes.size() - 1; i >= 0; i--) { - auto& node = active().graph.nodes[i]; - if (node.imported || node.shadow) continue; - if (mc.x >= node.position.x && mc.x <= node.position.x + node.size.x && - mc.y >= node.position.y && mc.y <= node.position.y + node.size.y) - return node.id; - } - return -1; - }; - - // --- Double-click on node: edit --- - if (canvas_hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { - ImVec2 mc = screen_to_canvas(mouse_pos, canvas_origin); - int hit_id = hit_test_node(mc); - if (hit_id >= 0) { - for (auto& node : active().graph.nodes) { - if (node.id == hit_id) { - editing_node_ = node.id; - creating_new_node_ = false; - dragging_node_ = -1; - edit_buf_ = node.edit_text(); - edit_just_opened_ = true; - break; - } - } - } - } - // --- Single click --- - else if (canvas_hovered && editing_link_ < 0 && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { - if (editing_node_ >= 0) { - if (creating_new_node_ && editing_node_ > 0) active().graph.remove_node(editing_node_); - editing_node_ = -1; - creating_new_node_ = false; - active().selected_nodes.clear(); - } else { - auto pin_hit = hit_test_pin(mouse_pos, canvas_origin); - if (!pin_hit.pin_id.empty()) { - // Start new link from any pin - dragging_link_from_pin_ = pin_hit.pin_id; - // All pins can be drag sources — direction determined at drop time - dragging_link_from_output_ = true; // will be refined at drop - dragging_node_ = -1; - dragging_selection_ = false; - } else { - dragging_link_from_pin_.clear(); - ImVec2 mc = screen_to_canvas(mouse_pos, canvas_origin); - int hit_id = hit_test_node(mc); - - if (hit_id >= 0) { - if (active().selected_nodes.count(hit_id)) { - // Clicking an already-selected node: start dragging selection - dragging_selection_ = true; - dragging_node_ = -1; - } else { - // Click unselected node: select only this one - active().selected_nodes.clear(); - active().selected_nodes.insert(hit_id); - dragging_selection_ = true; - dragging_node_ = -1; - } - } else { - // Check if clicking a wire — if so, don't start box select - int wire_hit = hit_test_link(mouse_pos, canvas_origin); - if (wire_hit >= 0) { - // Wire clicked — will be handled by link rename on mouse up - dragging_node_ = -1; - dragging_selection_ = false; - } else { - // Click empty space: start potential box select - // If released without dragging: deselect (if selected) or create node - box_selecting_ = true; - box_select_start_ = mouse_pos; - dragging_node_ = -1; - dragging_selection_ = false; - } - } - } - } - } - - // --- Box selection --- - if (box_selecting_) { - if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { - float dx = mouse_pos.x - box_select_start_.x; - float dy = mouse_pos.y - box_select_start_.y; - float dist = dx*dx + dy*dy; - // Only draw box if dragged more than a few pixels - if (dist > 25.0f) { - ImVec2 a = box_select_start_; - ImVec2 b = mouse_pos; - ImVec2 tl_box = {std::min(a.x, b.x), std::min(a.y, b.y)}; - ImVec2 br_box = {std::max(a.x, b.x), std::max(a.y, b.y)}; - dl->AddRectFilled(tl_box, br_box, IM_COL32(100, 150, 255, 40)); - dl->AddRect(tl_box, br_box, IM_COL32(100, 150, 255, 180)); - - ImVec2 ca = screen_to_canvas(tl_box, canvas_origin); - ImVec2 cb = screen_to_canvas(br_box, canvas_origin); - active().selected_nodes.clear(); - for (auto& node : active().graph.nodes) { - if (node.imported || node.shadow) continue; - if (node.position.x + node.size.x >= ca.x && node.position.x <= cb.x && - node.position.y + node.size.y >= ca.y && node.position.y <= cb.y) - active().selected_nodes.insert(node.id); - } - } - } else { - // Released: if didn't drag much, deselect or create node - float dx = mouse_pos.x - box_select_start_.x; - float dy = mouse_pos.y - box_select_start_.y; - if (dx*dx + dy*dy <= 25.0f) { - if (!active().selected_nodes.empty()) { - // Had selection: just deselect - active().selected_nodes.clear(); - } else { - // No selection: open editor for a new node (node created on commit) - creating_new_node_ = true; - editing_node_ = 0; // sentinel: no real node yet - new_node_pos_ = screen_to_canvas(mouse_pos, canvas_origin); - edit_buf_.clear(); - edit_just_opened_ = true; - } - } - box_selecting_ = false; - } - } - - // Link dragging - if (!dragging_link_from_pin_.empty()) { - if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) { - auto pin_hit = hit_test_pin(mouse_pos, canvas_origin); - if (!pin_hit.pin_id.empty() && pin_hit.pin_id != dragging_link_from_pin_) { - // Determine link direction from pin pair. - // Pure sources: Output, BangNext, LambdaGrab - // Pure destinations: Input, Lambda - // Bidirectional: BangTrigger (destination for bang chains, source for () -> void values) - auto from_dir = FlowPin::Input; // direction of the drag-start pin - for (auto& node : active().graph.nodes) { - for (auto& p : node.triggers) if (p->id == dragging_link_from_pin_) from_dir = p->direction; - for (auto& p : node.inputs) if (p->id == dragging_link_from_pin_) from_dir = p->direction; - for (auto& p : node.outputs) if (p->id == dragging_link_from_pin_) from_dir = p->direction; - for (auto& p : node.nexts) if (p->id == dragging_link_from_pin_) from_dir = p->direction; - if (node.lambda_grab.id == dragging_link_from_pin_) from_dir = node.lambda_grab.direction; - if (node.bang_pin.id == dragging_link_from_pin_) from_dir = node.bang_pin.direction; - } - - auto is_source = [](FlowPin::Direction d) { - return d == FlowPin::Output || d == FlowPin::BangNext || - d == FlowPin::LambdaGrab || d == FlowPin::BangTrigger; - }; - auto is_dest = [](FlowPin::Direction d) { - return d == FlowPin::Input || d == FlowPin::BangTrigger || - d == FlowPin::Lambda; - }; - - // Try both orientations — prefer the one that makes sense - std::string from_pin, to_pin; - bool valid = false; - if (is_source(from_dir) && is_dest(pin_hit.dir)) { - from_pin = dragging_link_from_pin_; - to_pin = pin_hit.pin_id; - valid = true; - } else if (is_source(pin_hit.dir) && is_dest(from_dir)) { - from_pin = pin_hit.pin_id; - to_pin = dragging_link_from_pin_; - valid = true; - } - - if (valid) { - // BangTrigger and Lambda allow multiple incoming connections - // (validation happens in inference, not here) - FlowPin::Direction to_dir = FlowPin::Input; - for (auto& node : active().graph.nodes) { - for (auto& p : node.triggers) if (p->id == to_pin) to_dir = FlowPin::BangTrigger; - for (auto& p : node.inputs) if (p->id == to_pin) to_dir = p->direction; - } - bool allow_multi = (to_dir == FlowPin::BangTrigger || to_dir == FlowPin::Lambda); - if (!allow_multi) - std::erase_if(active().graph.links, [&](auto& l) { return l.to_pin == to_pin; }); - active().graph.add_link(from_pin, to_pin); - mark_dirty(); - } - } - dragging_link_from_pin_.clear(); - } - } - - // Grab pending: waiting for drag threshold before detaching links (right mouse) - if (grab_pending_ && !grabbed_pin_.empty()) { - if (ImGui::IsMouseDown(ImGuiMouseButton_Right)) { - float dx = mouse_pos.x - grab_start_.x; - float dy = mouse_pos.y - grab_start_.y; - if (dx*dx + dy*dy > 25.0f) { - grab_pending_ = false; - for (auto& l : active().graph.links) { - if (grab_is_output_) { - if (l.from_pin == grabbed_pin_) - grabbed_links_.push_back({l.from_pin, l.to_pin}); - } else { - if (l.to_pin == grabbed_pin_) - grabbed_links_.push_back({l.from_pin, l.to_pin}); - } - } - if (!grabbed_links_.empty()) { - if (grab_is_output_) - std::erase_if(active().graph.links, [&](auto& l) { return l.from_pin == grabbed_pin_; }); - else - std::erase_if(active().graph.links, [&](auto& l) { return l.to_pin == grabbed_pin_; }); - active().graph.dirty = true; - } else { - grabbed_pin_.clear(); - } - } - } else { - grab_pending_ = false; - grabbed_pin_.clear(); - } - } - - // Grabbed links: actively dragging detached connections (right mouse) - if (!grabbed_links_.empty() && !grab_pending_) { - if (ImGui::IsMouseDown(ImGuiMouseButton_Right)) { - for (auto& gl : grabbed_links_) { - // Find the anchored end position (the end NOT being dragged) - ImVec2 anchor = {}; - bool found = false; - std::string anchor_id = grab_is_output_ ? gl.to_pin : gl.from_pin; - for (auto& n : active().graph.nodes) { - for (auto& p : n.outputs) if (p->id == anchor_id) { anchor = get_pin_pos(n, *p, canvas_origin); found = true; } - for (auto& p : n.nexts) if (p->id == anchor_id) { anchor = get_pin_pos(n, *p, canvas_origin); found = true; } - if (n.lambda_grab.id == anchor_id) { anchor = get_pin_pos(n, n.lambda_grab, canvas_origin); found = true; } - if (n.bang_pin.id == anchor_id) { anchor = get_pin_pos(n, n.bang_pin, canvas_origin); found = true; } - for (auto& p : n.triggers) if (p->id == anchor_id) { anchor = get_pin_pos(n, *p, canvas_origin); found = true; } - for (auto& p : n.inputs) if (p->id == anchor_id) { anchor = get_pin_pos(n, *p, canvas_origin); found = true; } - } - if (found) { - ImU32 col = COL_LINK_DRAG; - if (grab_is_output_) - draw_vbezier(dl, mouse_pos, anchor, col, 2.5f, active().canvas_zoom); - else - draw_vbezier(dl, anchor, mouse_pos, col, 2.5f, active().canvas_zoom); - } - } - } else { - // Released: try to reconnect - auto pin_hit = hit_test_pin(mouse_pos, canvas_origin); - bool reconnected = false; - if (!pin_hit.pin_id.empty()) { - if (grab_is_output_) { - // Was dragging source side: drop on another source pin - if (pin_hit.dir == FlowPin::Output || pin_hit.dir == FlowPin::BangNext || pin_hit.dir == FlowPin::LambdaGrab) { - for (auto& gl : grabbed_links_) - active().graph.add_link(pin_hit.pin_id, gl.to_pin); - reconnected = true; - mark_dirty(); - } - } else { - // Was dragging dest side: drop on another dest pin - if (pin_hit.dir == FlowPin::Input || pin_hit.dir == FlowPin::BangTrigger || pin_hit.dir == FlowPin::Lambda) { - // BangTrigger and Lambda allow multiple — don't erase - if (pin_hit.dir != FlowPin::BangTrigger && pin_hit.dir != FlowPin::Lambda) - std::erase_if(active().graph.links, [&](auto& l) { return l.to_pin == pin_hit.pin_id; }); - for (auto& gl : grabbed_links_) - active().graph.add_link(gl.from_pin, pin_hit.pin_id); - reconnected = true; - mark_dirty(); - } - } - } - if (!reconnected) { - // Put links back where they were - for (auto& gl : grabbed_links_) - active().graph.add_link(gl.from_pin, gl.to_pin); - } - grabbed_links_.clear(); - grabbed_pin_.clear(); - } - } - - // Selection dragging (move all selected nodes) - if (dragging_selection_ && ImGui::IsMouseDown(ImGuiMouseButton_Left)) { - ImVec2 delta = ImGui::GetIO().MouseDelta; - for (auto& node : active().graph.nodes) { - if (active().selected_nodes.count(node.id)) { - node.position.x += delta.x / active().canvas_zoom; - node.position.y += delta.y / active().canvas_zoom; - } - } - } - if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { - if (dragging_selection_) mark_dirty(); - dragging_selection_ = false; - dragging_node_ = -1; - } - - // --- Keyboard shortcuts --- - if (canvas_hovered && editing_node_ < 0) { - bool ctrl = ImGui::GetIO().KeyCtrl; - if (ctrl && ImGui::IsKeyPressed(ImGuiKey_C)) { - copy_selection(); - } - if (ctrl && ImGui::IsKeyPressed(ImGuiKey_V)) { - ImVec2 mc = screen_to_canvas(mouse_pos, canvas_origin); - paste_at(mc); - } - if (ctrl && ImGui::IsKeyPressed(ImGuiKey_D)) { - // Duplicate: copy + paste at mouse, without affecting clipboard - auto saved_nodes = active().clipboard_nodes; - auto saved_links = active().clipboard_links; - copy_selection(); - ImVec2 mc = screen_to_canvas(mouse_pos, canvas_origin); - paste_at(mc); - active().clipboard_nodes = saved_nodes; - active().clipboard_links = saved_links; - } - if (ImGui::IsKeyPressed(ImGuiKey_Delete) && !active().selected_nodes.empty()) { - for (int id : active().selected_nodes) - active().graph.remove_node(id); - active().selected_nodes.clear(); - mark_dirty(); - } - if (ctrl && ImGui::IsKeyPressed(ImGuiKey_Z)) { - if (ImGui::GetIO().KeyShift) - redo(); - else - undo(); - active().selected_nodes.clear(); - } - if (ctrl && ImGui::IsKeyPressed(ImGuiKey_Y)) { - redo(); - active().selected_nodes.clear(); - } - } - - // --- Right click: track start position and check for pin grab --- - static ImVec2 right_click_start = {}; - if (canvas_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { - right_click_start = mouse_pos; - // Check if right-clicking a pin with connections -> potential grab - auto pin_hit = hit_test_pin(mouse_pos, canvas_origin); - if (!pin_hit.pin_id.empty()) { - grabbed_links_.clear(); - grabbed_pin_ = pin_hit.pin_id; - grab_is_output_ = (pin_hit.dir == FlowPin::Output || pin_hit.dir == FlowPin::BangNext || - pin_hit.dir == FlowPin::LambdaGrab); - grab_pending_ = true; - grab_start_ = mouse_pos; - } - } - - // --- Right click release: disconnect pin, delete link, or delete node (only if not dragged) --- - if (canvas_hovered && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) { - float rdx = mouse_pos.x - right_click_start.x; - float rdy = mouse_pos.y - right_click_start.y; - bool was_drag = (rdx*rdx + rdy*rdy > 25.0f); - - if (!was_drag) { - // First check if right-clicking a connected pin to disconnect - auto pin_hit = hit_test_pin(mouse_pos, canvas_origin); - if (!pin_hit.pin_id.empty()) { - // Remove all links to/from this pin - std::erase_if(active().graph.links, [&](auto& l) { - return l.from_pin == pin_hit.pin_id || l.to_pin == pin_hit.pin_id; - }); - active().graph.dirty = true; - } - // Then check links - else { - int lid = hit_test_link(mouse_pos, canvas_origin); - if (lid >= 0) { - active().graph.remove_link(lid); - } else { - // Check if right-clicking a node to delete it - ImVec2 mc = screen_to_canvas(mouse_pos, canvas_origin); - for (int i = (int)active().graph.nodes.size() - 1; i >= 0; i--) { - auto& node = active().graph.nodes[i]; - if (mc.x >= node.position.x && mc.x <= node.position.x + node.size.x && - mc.y >= node.position.y && mc.y <= node.position.y + node.size.y) { - active().graph.remove_node(node.id); - if (editing_node_ == node.id) { - editing_node_ = -1; - creating_new_node_ = false; - } - break; - } - } - } - } - mark_dirty(); - } // !was_drag - } - - // --- Build shadow filter sets for drawing --- - std::set shadow_guids; - shadow_connected_pins_.clear(); - for (auto& node : active().graph.nodes) - if (node.shadow) shadow_guids.insert(node.guid); - for (auto& link : active().graph.links) { - auto d1 = link.from_pin.find('.'); - if (d1 != std::string::npos && shadow_guids.count(link.from_pin.substr(0, d1))) - shadow_connected_pins_.insert(link.to_pin); - auto d2 = link.to_pin.find('.'); - if (d2 != std::string::npos && shadow_guids.count(link.to_pin.substr(0, d2))) - shadow_connected_pins_.insert(link.from_pin); - } - - // --- Draw links (skip links involving shadow nodes) --- - for (auto& link : active().graph.links) { - auto d1 = link.from_pin.find('.'); - auto d2 = link.to_pin.find('.'); - if (d1 != std::string::npos && shadow_guids.count(link.from_pin.substr(0, d1))) continue; - if (d2 != std::string::npos && shadow_guids.count(link.to_pin.substr(0, d2))) continue; - draw_link(dl, link, canvas_origin); - } - - // --- Draw link being dragged --- - if (!dragging_link_from_pin_.empty() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) { - for (auto& node : active().graph.nodes) { - // Find the dragged pin position (any pin type) - ImVec2 from = {}; - bool from_grab = false; - bool from_bang_pin = false; - bool found = false; - for (auto& pin : node.outputs) { - if (pin->id == dragging_link_from_pin_) { from = get_pin_pos(node, *pin, canvas_origin); found = true; break; } - } - if (!found) for (auto& pin : node.nexts) { - if (pin->id == dragging_link_from_pin_) { from = get_pin_pos(node, *pin, canvas_origin); found = true; break; } - } - if (!found) for (auto& pin : node.inputs) { - if (pin->id == dragging_link_from_pin_) { from = get_pin_pos(node, *pin, canvas_origin); found = true; break; } - } - if (!found) for (auto& pin : node.triggers) { - if (pin->id == dragging_link_from_pin_) { from = get_pin_pos(node, *pin, canvas_origin); found = true; break; } - } - if (!found && node.lambda_grab.id == dragging_link_from_pin_) { - from = get_pin_pos(node, node.lambda_grab, canvas_origin); - found = true; - from_grab = true; - } - if (!found && node.bang_pin.id == dragging_link_from_pin_) { - from = get_pin_pos(node, node.bang_pin, canvas_origin); - found = true; - from_bang_pin = true; - } - if (found) { - auto target = hit_test_pin(mouse_pos, canvas_origin); - // Any different pin is a potential target — validation happens at drop - bool valid_target = !target.pin_id.empty() && target.pin_id != dragging_link_from_pin_; - ImU32 col = valid_target ? COL_PIN_HOVER : COL_LINK_DRAG; - if (from_grab) { - float dx = std::max(std::abs(mouse_pos.x - from.x) * 0.5f, 30.0f * active().canvas_zoom); - float dy = std::max(std::abs(mouse_pos.y - from.y) * 0.5f, 30.0f * active().canvas_zoom); - dl->AddBezierCubic(from, {from.x - dx, from.y}, {mouse_pos.x, mouse_pos.y - dy}, - mouse_pos, col, 2.5f * active().canvas_zoom); - } else if (from_bang_pin) { - float dx = std::max(std::abs(mouse_pos.x - from.x) * 0.5f, 30.0f * active().canvas_zoom); - dl->AddBezierCubic(from, {from.x + dx, from.y}, {mouse_pos.x - dx, mouse_pos.y}, - mouse_pos, col, 2.5f * active().canvas_zoom); - } else { - draw_vbezier(dl, from, mouse_pos, col, 2.5f, active().canvas_zoom); - } - goto done_drag; - } - } - done_drag:; - } - - // --- Draw nodes --- - auto hovered_pin = hit_test_pin(mouse_pos, canvas_origin); - for (auto& node : active().graph.nodes) { - if (node.imported || node.shadow) continue; - draw_node(dl, node, canvas_origin); - } - - // Pin hover highlight - if (!hovered_pin.pin_id.empty()) { - for (auto& node : active().graph.nodes) { - PinShape io_shape = PinShape::Signal; - float pr = PIN_RADIUS * active().canvas_zoom; - auto check = [&](auto& pins, PinShape shape) { - for (auto& pin : pins) - if (pin->id == hovered_pin.pin_id) { - ImVec2 pp = get_pin_pos(node, *pin, canvas_origin); - draw_pin_highlight(dl, pp, pr, COL_PIN_HOVER, shape, active().canvas_zoom); - } - }; - check(node.triggers, PinShape::Square); - // Inputs: check each pin's direction for shape - for (auto& pin : node.inputs) - if (pin->id == hovered_pin.pin_id) { - ImVec2 pp = get_pin_pos(node, *pin, canvas_origin); - PinShape shape = (pin->direction == FlowPin::Lambda) ? PinShape::LambdaDown : io_shape; - draw_pin_highlight(dl, pp, pr, COL_PIN_HOVER, shape, active().canvas_zoom); - } - check(node.nexts, PinShape::Square); - check(node.outputs, io_shape); - if (node.lambda_grab.id == hovered_pin.pin_id) { - ImVec2 pp = get_pin_pos(node, node.lambda_grab, canvas_origin); - draw_pin_highlight(dl, pp, pr, COL_PIN_HOVER, PinShape::LambdaLeft, active().canvas_zoom); - } - } - } - - // --- Tooltips --- - if (canvas_hovered && editing_node_ < 0 && editing_link_ < 0) { - if (!hovered_pin.pin_id.empty()) { - // Pin tooltip - for (auto& node : active().graph.nodes) { - if (node.id != hovered_pin.node_id) continue; - // Find the pin object - auto find_pin = [&](auto& pins) -> const FlowPin* { - for (auto& p : pins) if (p->id == hovered_pin.pin_id) return p.get(); - return nullptr; - }; - const FlowPin* pin = find_pin(node.triggers); - if (!pin) pin = find_pin(node.inputs); - if (!pin) pin = find_pin(node.outputs); - if (!pin) pin = find_pin(node.nexts); - if (!pin && node.lambda_grab.id == hovered_pin.pin_id) pin = &node.lambda_grab; - if (!pin && node.bang_pin.id == hovered_pin.pin_id) pin = &node.bang_pin; - if (pin) { - auto [port_name, port_desc] = get_port_desc(node, *pin); - std::string type_str; - if (pin->resolved_type) - type_str = type_to_string(pin->resolved_type); - else if (pin->direction == FlowPin::BangTrigger || pin->direction == FlowPin::BangNext) - type_str = "bang"; - else - type_str = "?"; - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(active().canvas_zoom); - ImGui::TextUnformatted((port_name + " : " + type_str).c_str()); - if (!port_desc.empty()) - ImGui::TextDisabled("%s", port_desc.c_str()); - ImGui::EndTooltip(); - } - break; - } - } else { - // Check link hover - int lid = hit_test_link(mouse_pos, canvas_origin); - if (lid >= 0) { - // Find the link - for (auto& link : active().graph.links) { - if (link.id != lid) continue; - std::string from_label, to_label; - for (auto& n : active().graph.nodes) { - for (auto& p : n.outputs) if (p->id == link.from_pin) from_label = pin_label(n, *p); - for (auto& p : n.nexts) if (p->id == link.from_pin) from_label = pin_label(n, *p); - for (auto& p : n.triggers) if (p->id == link.from_pin) from_label = pin_label(n, *p); - if (n.lambda_grab.id == link.from_pin) from_label = pin_label(n, n.lambda_grab); - if (n.bang_pin.id == link.from_pin) from_label = pin_label(n, n.bang_pin); - for (auto& p : n.inputs) if (p->id == link.to_pin) to_label = pin_label(n, *p); - for (auto& p : n.triggers) if (p->id == link.to_pin) to_label = pin_label(n, *p); - } - if (!from_label.empty() && !to_label.empty()) { - // Get types for the link endpoints - auto* fp = active().graph.find_pin(link.from_pin); - auto* tp = active().graph.find_pin(link.to_pin); - std::string from_type_str = (fp && fp->resolved_type) ? type_to_string(fp->resolved_type) : "?"; - std::string to_type_str = (tp && tp->resolved_type) ? type_to_string(tp->resolved_type) : "?"; - bool type_err = !link.error.empty(); - if (!type_err && fp && tp && fp->resolved_type && tp->resolved_type && - !fp->resolved_type->is_generic && !tp->resolved_type->is_generic) - type_err = !types_compatible(fp->resolved_type, tp->resolved_type); - - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(active().canvas_zoom); - // Show net name prominently if it has one - if (!link.net_name.empty()) { - ImGui::TextColored({0.7f, 0.9f, 1.0f, 1.0f}, "%s", link.net_name.c_str()); - } - ImGui::TextUnformatted((from_label + " -> " + to_label).c_str()); - ImGui::TextDisabled("%s -> %s", from_type_str.c_str(), to_type_str.c_str()); - if (!link.error.empty()) - ImGui::TextColored({1.0f, 0.2f, 0.2f, 1.0f}, "%s", link.error.c_str()); - else if (type_err) - ImGui::TextColored({1.0f, 0.2f, 0.2f, 1.0f}, "Type mismatch!"); - ImGui::TextDisabled("Click to rename wire"); - ImGui::EndTooltip(); - - // Left-click on wire opens rename editor (on release) - if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { - editing_link_ = link.id; - link_edit_buf_ = link.net_name.empty() ? "$" : link.net_name; - link_edit_just_opened_ = true; - } - } - break; - } - } else { - // Check node hover - ImVec2 mc = screen_to_canvas(mouse_pos, canvas_origin); - for (int i = (int)active().graph.nodes.size() - 1; i >= 0; i--) { - auto& node = active().graph.nodes[i]; - if (mc.x >= node.position.x && mc.x <= node.position.x + node.size.x && - mc.y >= node.position.y && mc.y <= node.position.y + node.size.y) { - auto* nt = find_node_type(node.type_id); - ImGui::BeginTooltip(); - ImGui::SetWindowFontScale(active().canvas_zoom); - ImGui::TextUnformatted(node_display_name(node).c_str()); - if (nt && nt->desc) - ImGui::TextDisabled("%s", nt->desc); - if (!node.error.empty()) { - ImGui::Separator(); - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 100, 100, 255)); - ImGui::TextUnformatted("Errors:"); - ImGui::TextUnformatted(node.error.c_str()); - ImGui::PopStyleColor(); - } - ImGui::TextDisabled("(%s)", node.guid.c_str()); - ImGui::EndTooltip(); - break; - } - } - } - } + if (active().pane) { + active().pane->draw(); } + ImGui::EndChild(); - // --- Name editing: inline inside the node --- - if (editing_node_ >= 0) { - // Find the node, or use new_node_pos_ for pending new nodes - FlowNode* edit_node = nullptr; - for (auto& node : active().graph.nodes) { - if (node.id == editing_node_) { edit_node = &node; break; } - } - ImVec2 edit_pos = edit_node ? to_imvec(edit_node->position) : new_node_pos_; - ImVec2 edit_size = edit_node ? to_imvec(edit_node->size) : ImVec2{NODE_MIN_WIDTH, NODE_HEIGHT}; - - { - ImVec2 tl = canvas_to_screen(edit_pos, canvas_origin); - ImVec2 br = canvas_to_screen({edit_pos.x + edit_size.x, - edit_pos.y + edit_size.y}, canvas_origin); - float nw = br.x - tl.x; - - float text_w = ImGui::CalcTextSize(edit_buf_.c_str()).x * active().canvas_zoom + 40.0f * active().canvas_zoom; - float scaled_min_w = std::max({nw, 160.0f * active().canvas_zoom, text_w}); - ImGui::SetNextWindowPos(tl); - ImGui::SetNextWindowSize({scaled_min_w, 0}); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {2 * active().canvas_zoom, 2 * active().canvas_zoom}); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {4 * active().canvas_zoom, 2 * active().canvas_zoom}); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {4 * active().canvas_zoom, 2 * active().canvas_zoom}); - ImGui::Begin("##name_edit", nullptr, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar); - ImGui::SetWindowFontScale(active().canvas_zoom); - - if (edit_just_opened_) { - ImGui::SetKeyboardFocusHere(); - edit_just_opened_ = false; - } - - char buf[128]; - strncpy(buf, edit_buf_.c_str(), sizeof(buf) - 1); - buf[sizeof(buf) - 1] = '\0'; - - // Callback to move cursor to end after autocomplete - bool* cursor_to_end_ptr = &edit_cursor_to_end_; - auto edit_callback = [](ImGuiInputTextCallbackData* data) -> int { - bool* flag = (bool*)data->UserData; - if (*flag) { - data->CursorPos = data->BufTextLen; - data->SelectionStart = data->SelectionEnd = data->CursorPos; - *flag = false; - } - return 0; - }; - - ImGui::SetNextItemWidth(-1); - bool committed = ImGui::InputText("##edit", buf, sizeof(buf), - ImGuiInputTextFlags_EnterReturnsTrue | - ImGuiInputTextFlags_CallbackAlways, - edit_callback, cursor_to_end_ptr); - edit_buf_ = buf; - - // Split into first word (type name) and rest (args) for matching - std::string first_word = edit_buf_; - std::string rest_args; - auto space_pos = edit_buf_.find(' '); - if (space_pos != std::string::npos) { - first_word = edit_buf_.substr(0, space_pos); - rest_args = edit_buf_.substr(space_pos + 1); - } - - // Autocompletion: match against first word, show all when empty - // Only show when no space yet (still typing the type name) - if (space_pos == std::string::npos) { - for (int i = 0; i < NUM_NODE_TYPES; i++) { - std::string nt_name(NODE_TYPES[i].name); - if (first_word.empty() || (nt_name.find(first_word) != std::string::npos && nt_name != first_word)) { - if (ImGui::Selectable(NODE_TYPES[i].name)) { - // Insert type + space, keep editor open - edit_buf_ = nt_name + " "; - edit_just_opened_ = true; // re-focus the text input next frame - edit_cursor_to_end_ = true; // place cursor at end, not select-all - } - } - } - } - - if (committed) do { - std::string first_word, rest_args; - auto sp = edit_buf_.find(' '); - if (sp != std::string::npos) { - first_word = edit_buf_.substr(0, sp); - rest_args = edit_buf_.substr(sp + 1); - } else { - first_word = edit_buf_; - } - - std::string node_type = first_word; - if (node_type.empty()) break; - - // If this is a pending new node (no backing node yet), create it now - if (creating_new_node_ && !edit_node) { - int id = active().graph.add_node("", to_vec2(new_node_pos_), 0, 0); - for (auto& n : active().graph.nodes) { - if (n.id == id) { edit_node = &n; break; } - } - editing_node_ = id; - } - if (!edit_node) break; - - auto* nt = find_node_type(node_type.c_str()); - if (!nt) { - // Unknown type: treat entire input as an expr node - nt = find_node_type("expr"); - node_type = "expr"; - rest_args = edit_buf_; - } - int default_triggers = nt ? nt->num_triggers : 0; - int default_inputs = nt ? nt->inputs : 0; - int default_outputs = nt ? nt->outputs : 0; - int default_nexts = nt ? nt->num_nexts : 0; - - // Auto-assign guid if not set - auto& node = *edit_node; - if (node.guid.empty()) - node.guid = generate_guid(); - node.type_id = node_type_id_from_string(node_type.c_str()); - node.args = rest_args; - node.parse_args(); - active().graph.dirty = true; - creating_new_node_ = false; - - // Resize a pin vector: reuse existing pins (preserving IDs/links), - // add new ones at end, remove excess from end (clearing their links). - auto resize_pins = [&](PinVec& pins, int needed, - const std::vector& names, - FlowPin::Direction dir, bool is_output) { - // Reuse existing: just rename - for (int i = 0; i < std::min((int)pins.size(), needed); i++) - pins[i]->name = names[i]; - // Add new - for (int i = (int)pins.size(); i < needed; i++) - pins.push_back(make_pin("", names[i], "", nullptr, dir)); - // Remove excess (from back) - while ((int)pins.size() > needed) { - auto pid = pins.back()->id; - if (is_output) - std::erase_if(active().graph.links, [&pid](auto& l) { return l.from_pin == pid; }); - else - std::erase_if(active().graph.links, [&pid](auto& l) { return l.to_pin == pid; }); - pins.pop_back(); - } - }; - - auto make_names = [](const std::string& prefix, int count) { - std::vector names; - for (int i = 0; i < count; i++) names.push_back(prefix + std::to_string(i)); - return names; - }; - - int needed_outputs = default_outputs; - bool is_expr_type = is_any_of(node.type_id, NodeTypeID::Expr, NodeTypeID::ExprBang); - - // Build desired input pin list (data + lambda unified, in slot order) - struct DesiredPin { std::string name; FlowPin::Direction dir; }; - std::vector desired_inputs; - - if (node.type_id == NodeTypeID::New) { - auto tokens = tokenize_args(rest_args, false); - std::string inst_type_name = tokens.empty() ? "" : tokens[0]; - auto* type_node = find_type_node(active().graph, inst_type_name); - if (type_node) { - auto fields = parse_type_fields(*type_node); - for (auto& field : fields) - desired_inputs.push_back({field.name, FlowPin::Input}); - } - needed_outputs = 1; - } else if (node.type_id == NodeTypeID::EventBang) { - // Outputs come from event declaration args - auto tokens = tokenize_args(rest_args, false); - std::string event_name = tokens.empty() ? "" : tokens[0]; - auto* event_decl = find_event_node(active().graph, event_name); - if (event_decl) { - auto args = parse_event_args(*event_decl, active().graph); - // Override outputs - std::vector out_names; - for (auto& a : args) out_names.push_back(a.name); - needed_outputs = (int)out_names.size(); - // Resize outputs directly here - for (int i = 0; i < std::min((int)node.outputs.size(), needed_outputs); i++) - node.outputs[i]->name = out_names[i]; - for (int i = (int)node.outputs.size(); i < needed_outputs; i++) - node.outputs.push_back(make_pin("", out_names[i], "", nullptr, FlowPin::Output)); - while ((int)node.outputs.size() > needed_outputs) { - auto pid = node.outputs.back()->id; - std::erase_if(active().graph.links, [&pid](auto& l) { return l.from_pin == pid; }); - node.outputs.pop_back(); - } - needed_outputs = -1; // skip generic output resize below - } - } else { - if (is_expr_type) { - // Expr nodes: pin count from $N refs, output count from tokens - auto parsed = scan_slots(rest_args); - int total_top = parsed.total_pin_count(default_inputs); - for (int i = 0; i < total_top; i++) { - bool is_lambda = parsed.is_lambda_slot(i); - std::string pin_name = is_lambda ? ("@" + std::to_string(i)) : std::to_string(i); - desired_inputs.push_back({pin_name, is_lambda ? FlowPin::Lambda : FlowPin::Input}); - } - if (!node.args.empty()) { - auto tokens = tokenize_args(rest_args, false); - needed_outputs = std::max(1, (int)tokens.size()); - } - } else if (node_type == "cast" || node_type == "new") { - // Args are type names — use descriptor defaults directly - for (int i = 0; i < default_inputs; i++) { - std::string pin_name; - bool is_lambda = false; - if (nt && nt->input_ports && i < nt->inputs) { - pin_name = nt->input_ports[i].name; - is_lambda = (nt->input_ports[i].kind == PortKind::Lambda); - } else { - pin_name = std::to_string(i); - } - desired_inputs.push_back({pin_name, is_lambda ? FlowPin::Lambda : FlowPin::Input}); - } - } else { - // Non-expr nodes: use inline arg computation - auto info = compute_inline_args(rest_args, default_inputs); - if (!info.error.empty()) node.error = info.error; - // First: $N/@N ref pins - int ref_pins = (info.pin_slots.max_slot >= 0) ? (info.pin_slots.max_slot + 1) : 0; - for (int i = 0; i < ref_pins; i++) { - bool is_lambda = info.pin_slots.is_lambda_slot(i); - std::string pin_name = is_lambda ? ("@" + std::to_string(i)) : std::to_string(i); - desired_inputs.push_back({pin_name, is_lambda ? FlowPin::Lambda : FlowPin::Input}); - } - // Then: remaining descriptor inputs - for (int i = info.num_inline_args; i < default_inputs; i++) { - std::string pin_name; - bool is_lambda = false; - if (nt && nt->input_ports && i < nt->inputs) { - pin_name = nt->input_ports[i].name; - is_lambda = (nt->input_ports[i].kind == PortKind::Lambda); - } else { - pin_name = std::to_string(i); - } - desired_inputs.push_back({pin_name, is_lambda ? FlowPin::Lambda : FlowPin::Input}); - } - } - } - - // Resize inputs (unified data + lambda), preserving connections - { - int needed = (int)desired_inputs.size(); - // Reuse existing: update name and direction - for (int i = 0; i < std::min((int)node.inputs.size(), needed); i++) { - node.inputs[i]->name = desired_inputs[i].name; - node.inputs[i]->direction = desired_inputs[i].dir; - } - // Add new - for (int i = (int)node.inputs.size(); i < needed; i++) - node.inputs.push_back(make_pin("", desired_inputs[i].name, "", nullptr, desired_inputs[i].dir)); - // Remove excess - while ((int)node.inputs.size() > needed) { - auto pid = node.inputs.back()->id; - std::erase_if(active().graph.links, [&pid](auto& l) { return l.to_pin == pid; }); - node.inputs.pop_back(); - } - } - - // Resize bang inputs - resize_pins(node.triggers, default_triggers, - make_names("bang_in", default_triggers), FlowPin::BangTrigger, false); - if (needed_outputs >= 0) - resize_pins(node.outputs, needed_outputs, - make_names("out", needed_outputs), FlowPin::Output, true); - resize_pins(node.nexts, default_nexts, - make_names("bang", default_nexts), FlowPin::BangNext, true); - - // Rebuild pin IDs from guid and update links - // Collect old->new ID mapping for pins whose name changed - auto update_pin_ids = [&](PinVec& pins) { - for (auto& p : pins) { - std::string new_id = node.pin_id(p->name); - if (p->id != new_id) { - // Update any links referencing old ID - for (auto& l : active().graph.links) { - if (l.from_pin == p->id) l.from_pin = new_id; - if (l.to_pin == p->id) l.to_pin = new_id; - } - p->id = new_id; - } - } - }; - update_pin_ids(node.triggers); - update_pin_ids(node.inputs); - update_pin_ids(node.outputs); - update_pin_ids(node.nexts); - { - std::string new_id = node.pin_id("as_lambda"); - for (auto& l : active().graph.links) { - if (l.from_pin == node.lambda_grab.id) l.from_pin = new_id; - if (l.to_pin == node.lambda_grab.id) l.to_pin = new_id; - } - node.lambda_grab.id = new_id; - } - { - std::string new_id = node.pin_id("post_bang"); - for (auto& l : active().graph.links) { - if (l.from_pin == node.bang_pin.id) l.from_pin = new_id; - if (l.to_pin == node.bang_pin.id) l.to_pin = new_id; - } - node.bang_pin.id = new_id; - } - - // Generate shadow nodes for inline args and rebuild display text - update_shadows_for_node(active().graph, node, rest_args); - - editing_node_ = -1; - mark_dirty(); - } while (false); - - if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { - if (creating_new_node_ && edit_node) { - active().graph.remove_node(editing_node_); - } - creating_new_node_ = false; - editing_node_ = -1; - } - - ImGui::End(); - ImGui::PopStyleVar(3); - } // end of edit window block - } - - // --- Wire name editing popup --- - if (editing_link_ >= 0) { - FlowLink* edit_link = nullptr; - for (auto& link : active().graph.links) { - if (link.id == editing_link_) { edit_link = &link; break; } - } - if (!edit_link) { - editing_link_ = -1; - } else { - // Position popup near the wire midpoint - ImVec2 fp = {}, tp = {}; - for (auto& n : active().graph.nodes) { - for (auto& p : n.outputs) if (p->id == edit_link->from_pin) fp = get_pin_pos(n, *p, canvas_origin); - for (auto& p : n.nexts) if (p->id == edit_link->from_pin) fp = get_pin_pos(n, *p, canvas_origin); - if (n.lambda_grab.id == edit_link->from_pin) fp = get_pin_pos(n, n.lambda_grab, canvas_origin); - if (n.bang_pin.id == edit_link->from_pin) fp = get_pin_pos(n, n.bang_pin, canvas_origin); - for (auto& p : n.inputs) if (p->id == edit_link->to_pin) tp = get_pin_pos(n, *p, canvas_origin); - for (auto& p : n.triggers) if (p->id == edit_link->to_pin) tp = get_pin_pos(n, *p, canvas_origin); - } - ImVec2 mid = {(fp.x + tp.x) * 0.5f, (fp.y + tp.y) * 0.5f}; - - float text_w = ImGui::CalcTextSize(link_edit_buf_.c_str()).x * active().canvas_zoom + 40.0f * active().canvas_zoom; - float popup_w = std::max(200.0f * active().canvas_zoom, text_w); - ImGui::SetNextWindowPos({mid.x - popup_w * 0.5f, mid.y - 15.0f * active().canvas_zoom}); - ImGui::SetNextWindowSize({popup_w, 0}); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {4 * active().canvas_zoom, 4 * active().canvas_zoom}); - ImGui::Begin("##wire_rename", nullptr, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar); - ImGui::SetWindowFontScale(active().canvas_zoom); - - bool was_just_opened = link_edit_just_opened_; - if (link_edit_just_opened_) { - ImGui::SetKeyboardFocusHere(); - link_edit_just_opened_ = false; - } - - char buf[128]; - strncpy(buf, link_edit_buf_.c_str(), sizeof(buf) - 1); - buf[sizeof(buf) - 1] = '\0'; - - bool committed = ImGui::InputText("##wire_name", buf, sizeof(buf), - ImGuiInputTextFlags_EnterReturnsTrue); - link_edit_buf_ = buf; - - // Validate: must start with $, must be unique among net names - bool valid = true; - std::string error_msg; - std::string new_name = link_edit_buf_; - if (new_name.empty() || new_name[0] != '$') { - valid = false; - error_msg = "Must start with $"; - } else if (new_name.size() < 2) { - valid = false; - error_msg = "Name too short"; - } else { - // Check uniqueness: no other link with a different source pin should have this net name - for (auto& other : active().graph.links) { - if (other.id == edit_link->id) continue; - if (other.net_name == new_name && other.from_pin != edit_link->from_pin) { - valid = false; - error_msg = "Name already in use"; - break; - } - } - } - - if (!valid && !error_msg.empty()) { - ImGui::TextColored({1.0f, 0.3f, 0.3f, 1.0f}, "%s", error_msg.c_str()); - } - - if (committed && valid) { - // Update net name on this link AND all links from the same source pin - std::string old_from = edit_link->from_pin; - for (auto& link : active().graph.links) { - if (link.from_pin == old_from) { - link.net_name = new_name; - link.auto_wire = false; - } - } - editing_link_ = -1; - rebuild_all_inline_display(active().graph); - mark_dirty(); - } - - if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { - editing_link_ = -1; - } - - // Dismiss if clicked outside the rename window (skip first frame) - if (!was_just_opened && - !ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && - ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { - editing_link_ = -1; - } - - ImGui::End(); - ImGui::PopStyleVar(1); - } - } - - ImGui::EndChild(); // flow_canvas (legacy) - } // end legacy Editor1 canvas - - // --- Horizontal splitter (between canvas and bottom panel) --- + // --- Horizontal splitter --- ImGui::InvisibleButton("##hsplitter", {canvas_w, 4.0f}); - if (ImGui::IsItemActive()) { + if (ImGui::IsItemActive()) bottom_panel_height_ -= ImGui::GetIO().MouseDelta.y; - } if (ImGui::IsItemHovered() || ImGui::IsItemActive()) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS); - // --- Bottom panel: tabbed (Errors / Build Log) --- - ImGui::BeginChild("##bottom_panel", {canvas_w, bottom_panel_height_}, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + // --- Bottom panel --- + auto* e1 = dynamic_cast(active().pane.get()); + ImGui::BeginChild("##bottom_panel", {canvas_w, bottom_panel_height_}, true, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); if (ImGui::BeginTabBar("##bottom_tabs")) { - // Count errors for tab label - int error_count = 0; - for (auto& node : active().graph.nodes) if (!node.error.empty()) error_count++; - for (auto& link : active().graph.links) if (!link.error.empty()) error_count++; - - char errors_label[64]; - snprintf(errors_label, sizeof(errors_label), "Errors%s", error_count > 0 ? " (!)" : ""); - - if (ImGui::BeginTabItem(errors_label)) { - ImGui::BeginChild("##errors_scroll", {0, 0}, false); - for (auto& node : active().graph.nodes) { - if (node.error.empty()) continue; - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 100, 100, 255)); - std::string label = std::string(node_type_str(node.type_id)) + " [" + node.guid.substr(0, 8) + "]: " + node.error; - if (ImGui::Selectable(label.c_str())) { - center_on_node(node, {canvas_w, canvas_h}); - } - ImGui::PopStyleColor(); - } - for (auto& link : active().graph.links) { - if (link.error.empty()) continue; - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 160, 80, 255)); - std::string label = "link [" + link.from_pin.substr(0, 8) + "->...]: " + link.error; - if (ImGui::Selectable(label.c_str())) { - auto dot = link.from_pin.find('.'); - if (dot != std::string::npos) { - std::string guid = link.from_pin.substr(0, dot); - for (auto& n : active().graph.nodes) { - if (n.guid == guid) { center_on_node(n, {canvas_w, canvas_h}); break; } + if (e1) { + int error_count = 0; + for (auto& node : e1->graph().nodes) if (!node.error.empty()) error_count++; + for (auto& link : e1->graph().links) if (!link.error.empty()) error_count++; + + char errors_label[64]; + snprintf(errors_label, sizeof(errors_label), "Errors%s", error_count > 0 ? " (!)" : ""); + + if (ImGui::BeginTabItem(errors_label)) { + ImGui::BeginChild("##errors_scroll", {0, 0}, false); + for (auto& node : e1->graph().nodes) { + if (node.error.empty()) continue; + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 100, 100, 255)); + std::string label = std::string(node_type_str(node.type_id)) + " [" + node.guid.substr(0, 8) + "]: " + node.error; + if (ImGui::Selectable(label.c_str())) { + e1->center_on_node(node, {canvas_w, canvas_h}); + } + ImGui::PopStyleColor(); + } + for (auto& link : e1->graph().links) { + if (link.error.empty()) continue; + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 160, 80, 255)); + std::string label = "link [" + link.from_pin.substr(0, 8) + "->...]: " + link.error; + if (ImGui::Selectable(label.c_str())) { + auto dot = link.from_pin.find('.'); + if (dot != std::string::npos) { + std::string guid = link.from_pin.substr(0, dot); + for (auto& n : e1->graph().nodes) { + if (n.guid == guid) { e1->center_on_node(n, {canvas_w, canvas_h}); break; } + } } } + ImGui::PopStyleColor(); } - ImGui::PopStyleColor(); + ImGui::Dummy({0, bottom_panel_height_ * 0.5f}); + ImGui::EndChild(); + ImGui::EndTabItem(); } - ImGui::Dummy({0, bottom_panel_height_ * 0.5f}); - ImGui::EndChild(); - ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Build Log", nullptr, show_build_log_ ? ImGuiTabItemFlags_SetSelected : 0)) { @@ -2087,7 +269,6 @@ void FlowEditorWindow::draw() { std::lock_guard lock(build_log_mutex_); ImGui::TextWrapped("%s", build_log_.c_str()); } - // Bottom padding so the last line isn't stuck at the edge ImGui::Dummy({0, bottom_panel_height_ * 0.5f}); if (build_state_ == BuildState::Building) { if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 40.0f) @@ -2105,393 +286,66 @@ void FlowEditorWindow::draw() { ImGui::SameLine(); - // --- Vertical splitter (between canvas column and side panel) --- + // --- Vertical splitter --- ImGui::InvisibleButton("##vsplitter", {4.0f, total_h}); - if (ImGui::IsItemActive()) { + if (ImGui::IsItemActive()) side_panel_width_ -= ImGui::GetIO().MouseDelta.x; - } if (ImGui::IsItemHovered() || ImGui::IsItemActive()) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW); ImGui::SameLine(); - // --- Side panel: declarations (right) --- + // --- Side panel: declarations --- ImGui::BeginChild("##side_panel", {side_panel_width_, total_h}, true); - ImGui::TextUnformatted("Declarations"); - ImGui::Separator(); - - // Local declarations (non-imported) - for (auto& node : active().graph.nodes) { - auto* nt_decl = find_node_type(node.type_id); - if (!nt_decl || !nt_decl->is_declaration) continue; - if (node.imported || node.shadow) continue; - bool has_err = !node.error.empty(); - if (has_err) ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 100, 100, 255)); - if (ImGui::Selectable(node.display_text().c_str())) { - center_on_node(node, {canvas_w, canvas_h}); - } - if (has_err) ImGui::PopStyleColor(); - if (has_err && ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::TextUnformatted(node.error.c_str()); - ImGui::EndTooltip(); - } - } - - // Imported declarations grouped by import source - // Collect unique import paths - for (auto& imp_node : active().graph.nodes) { - if (imp_node.type_id != NodeTypeID::DeclImport) continue; - auto tokens = tokenize_args(imp_node.args, false); - if (tokens.empty()) continue; - std::string label = tokens[0]; - // Strip quotes from string literal - if (label.size() >= 2 && label.front() == '"' && label.back() == '"') - label = label.substr(1, label.size() - 2); - if (ImGui::TreeNode(label.c_str())) { - for (auto& node : active().graph.nodes) { - if (!node.imported) continue; - auto* nt_decl = find_node_type(node.type_id); - if (!nt_decl || !nt_decl->is_declaration) continue; - ImGui::TextDisabled("%s", node.display_text().c_str()); - } - ImGui::TreePop(); - } - } - ImGui::EndChild(); - - ImGui::End(); // main - check_debounced_save(); - win_.end_frame(30, 30, 40); -} - -void FlowEditorWindow::validate_nodes() { - // Resolve type-based pins (new, event!) from current declarations - resolve_type_based_pins(active().graph); - - // Build type registry from decl_type nodes - TypeRegistry registry; - for (auto& node : active().graph.nodes) { - if (node.type_id == NodeTypeID::DeclType) { - auto tokens = tokenize_args(node.args, false); - if (tokens.size() >= 2) { - // First token is the type name, rest is the definition - std::string type_name = tokens[0]; - // Reconstruct the definition: for struct types, build field list - // For now, register the raw args minus the name - std::string def; - for (size_t i = 1; i < tokens.size(); i++) { - if (!def.empty()) def += " "; - def += tokens[i]; - } - int decl_class = classify_decl_type(tokens); - if (decl_class == 0 || decl_class == 1) { // alias or function type - registry.register_type(type_name, def); - } else { - registry.register_type(type_name, "void"); // placeholder, fields validated below - } - } - } - } - - // Resolve all types and check for cycles - registry.resolve_all(); - - for (auto& node : active().graph.nodes) { - node.error.clear(); - - auto* nt = find_node_type(node.type_id); - if (!nt) { - node.error = "Unknown node type: " + std::string(node_type_str(node.type_id)); - continue; - } - - // Check for duplicate guids - for (auto& other : active().graph.nodes) { - if (&other != &node && other.guid == node.guid) { - node.error = "Duplicate guid: " + node.guid; - break; - } - } - if (!node.error.empty()) continue; - - // Validate decl_type nodes - if (node.type_id == NodeTypeID::DeclType) { - auto tokens = tokenize_args(node.args, false); - if (tokens.empty()) { - node.error = "decl_type requires a type name"; - continue; - } - std::string type_name = tokens[0]; - if (!type_name.empty() && type_name[0] == '$') { - node.error = "Type name should not start with $"; - continue; - } - - // Check registry errors for this type - auto err_it = registry.errors.find(type_name); - if (err_it != registry.errors.end()) { - node.error = err_it->second; - continue; - } - - // Check struct types have fields - int decl_class_v = classify_decl_type(tokens); - if (decl_class_v == 2) { // struct - // Must be a struct — check it has at least one field - bool has_any_field = false; - for (size_t i = 1; i < tokens.size(); i++) { - if (tokens[i].find(':') != std::string::npos) { has_any_field = true; break; } - } - if (!has_any_field) { - node.error = "Struct type '" + type_name + "' must have at least one field (name:type)"; - continue; - } - } - - // Validate each field type - for (size_t i = 1; i < tokens.size(); i++) { - auto& tok = tokens[i]; - // Skip function syntax tokens - if (tok == "->" || tok[0] == '(') continue; - auto colon = tok.find(':'); - if (colon != std::string::npos) { - std::string field_type = tok.substr(colon + 1); - std::string err; - if (!registry.validate_type(field_type, err)) { - node.error = "Field '" + tok.substr(0, colon) + "': " + err; - break; - } - } - } - } - - // Validate decl_var nodes: decl_var - if (node.type_id == NodeTypeID::DeclVar) { - auto tokens = tokenize_args(node.args, false); - if (tokens.size() < 2) { - node.error = "decl_var requires: name type"; - continue; - } - // Check name doesn't start with $ - if (!tokens[0].empty() && tokens[0][0] == '$') { - node.error = "Variable name should not start with $ in declarations"; - continue; - } - // Validate type (second arg) - std::string err; - if (!registry.validate_type(tokens[1], err)) { - node.error = "Invalid type: " + err; - } - } - - - // Validate 'new' nodes — type must exist - if (node.type_id == NodeTypeID::New) { - auto tokens = tokenize_args(node.args, false); - if (tokens.empty()) { - node.error = "new requires a type name"; - continue; + if (e1) { + ImGui::TextUnformatted("Declarations"); + ImGui::Separator(); + for (auto& node : e1->graph().nodes) { + auto* nt_decl = find_node_type(node.type_id); + if (!nt_decl || !nt_decl->is_declaration) continue; + if (node.imported || node.shadow) continue; + bool has_err = !node.error.empty(); + if (has_err) ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 100, 100, 255)); + if (ImGui::Selectable(node.display_text().c_str())) { + e1->center_on_node(node, {canvas_w, canvas_h}); } - if (registry.type_defs.count(tokens[0]) == 0) { - node.error = "Unknown type: " + tokens[0]; + if (has_err) ImGui::PopStyleColor(); + if (has_err && ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(node.error.c_str()); + ImGui::EndTooltip(); } } - // Validate event! nodes — must reference a valid decl_event with ~ prefix, return must be void - if (node.type_id == NodeTypeID::EventBang) { - auto tokens = tokenize_args(node.args, false); - if (tokens.empty()) { - node.error = "event! requires an event name (e.g. ~my_event)"; - continue; - } - if (tokens[0].empty() || tokens[0][0] != '~') { - node.error = "Event name must start with ~ (e.g. ~" + tokens[0] + ")"; - continue; - } - auto* event_decl = find_event_node(active().graph, tokens[0]); - if (!event_decl) { - node.error = "Unknown event: " + tokens[0]; - continue; - } - // Check return type is void - auto ev_tokens = tokenize_args(event_decl->args, false); - bool found_arrow = false; - std::string ret_type; - for (size_t i = 1; i < ev_tokens.size(); i++) { - if (ev_tokens[i] == "->") { - found_arrow = true; - if (i + 1 < ev_tokens.size()) ret_type = ev_tokens[i + 1]; - break; + for (auto& imp_node : e1->graph().nodes) { + if (imp_node.type_id != NodeTypeID::DeclImport) continue; + auto tokens = tokenize_args(imp_node.args, false); + if (tokens.empty()) continue; + std::string label = tokens[0]; + if (label.size() >= 2 && label.front() == '"' && label.back() == '"') + label = label.substr(1, label.size() - 2); + if (ImGui::TreeNode(label.c_str())) { + for (auto& node : e1->graph().nodes) { + if (!node.imported) continue; + auto* nt_decl = find_node_type(node.type_id); + if (!nt_decl || !nt_decl->is_declaration) continue; + ImGui::TextDisabled("%s", node.display_text().c_str()); } - } - if (found_arrow && ret_type != "void") { - node.error = "Event return type must be void (got: " + ret_type + ")"; + ImGui::TreePop(); } } } + ImGui::EndChild(); - // Run type inference (always, since validate_nodes clears errors each frame) - run_type_inference(); -} - -void FlowEditorWindow::run_type_inference() { - GraphInference inference(active().type_pool); - inference.run(active().graph); -} - -void FlowEditorWindow::center_on_node(const FlowNode& node, ImVec2 canvas_size) { - active().canvas_offset.x = -node.position.x - node.size.x * 0.5f + canvas_size.x * 0.5f / active().canvas_zoom; - active().canvas_offset.y = -node.position.y - node.size.y * 0.5f + canvas_size.y * 0.5f / active().canvas_zoom; - active().highlight_node_id = node.id; - active().highlight_timer = 3.0f; -} - -void FlowEditorWindow::copy_selection() { - active().clipboard_nodes.clear(); - active().clipboard_links.clear(); - if (active().selected_nodes.empty()) return; - - // Compute centroid - ImVec2 centroid = {0, 0}; - int count = 0; - for (auto& node : active().graph.nodes) { - if (!active().selected_nodes.count(node.id)) continue; - centroid.x += node.position.x; - centroid.y += node.position.y; - count++; - } - if (count > 0) { centroid.x /= count; centroid.y /= count; } - - // Build index map: node id -> clipboard index - std::map id_to_idx; - for (auto& node : active().graph.nodes) { - if (!active().selected_nodes.count(node.id)) continue; - int idx = (int)active().clipboard_nodes.size(); - id_to_idx[node.id] = idx; - active().clipboard_nodes.push_back({node.type_id, node.args, - {node.position.x - centroid.x, node.position.y - centroid.y}}); - } - - // Copy internal links (both endpoints in selection) - // Build pin_id -> (node_id, pin_name) map - std::map> pin_owner; - for (auto& node : active().graph.nodes) { - if (!active().selected_nodes.count(node.id)) continue; - auto register_pin = [&](const FlowPin& p) { pin_owner[p.id] = {node.id, p.name}; }; - for (auto& p : node.triggers) register_pin(*p); - for (auto& p : node.inputs) register_pin(*p); - for (auto& p : node.outputs) register_pin(*p); - for (auto& p : node.nexts) register_pin(*p); - register_pin(node.lambda_grab); - register_pin(node.bang_pin); - } - for (auto& link : active().graph.links) { - auto fi = pin_owner.find(link.from_pin); - auto ti = pin_owner.find(link.to_pin); - if (fi != pin_owner.end() && ti != pin_owner.end()) { - auto from_idx = id_to_idx[fi->second.first]; - auto to_idx = id_to_idx[ti->second.first]; - active().clipboard_links.push_back({from_idx, to_idx, fi->second.second, ti->second.second}); - } - } -} - -void FlowEditorWindow::paste_at(ImVec2 canvas_pos) { - if (active().clipboard_nodes.empty()) return; - - active().selected_nodes.clear(); - std::vector new_guids; - - // Create nodes - for (auto& cn : active().clipboard_nodes) { - std::string guid = generate_guid(); - new_guids.push_back(guid); - ImVec2 pos = {canvas_pos.x + cn.offset.x, canvas_pos.y + cn.offset.y}; - int id = active().graph.add_node(guid, to_vec2(pos), 0, 0); - - // Set type and args, rebuild pins - for (auto& node : active().graph.nodes) { - if (node.id != id) continue; - node.type_id = cn.type_id; - node.args = cn.args; - node.parse_args(); - - // Rebuild pins from type descriptor - auto* nt = find_node_type(cn.type_id); - if (nt) { - node.triggers.clear(); - node.inputs.clear(); - node.outputs.clear(); - node.nexts.clear(); - - for (int i = 0; i < nt->num_triggers; i++) { - std::string biname = (nt->trigger_ports && i < nt->num_triggers) ? nt->trigger_ports[i].name : ("bang_in" + std::to_string(i)); - node.triggers.push_back(make_pin("", biname, "", nullptr, FlowPin::BangTrigger)); - } - - bool is_expr_paste = is_any_of(cn.type_id, NodeTypeID::Expr, NodeTypeID::ExprBang); - int num_outputs = nt->outputs; - if (is_expr_paste) { - auto parsed = scan_slots(cn.args); - int total_top = parsed.total_pin_count(nt->inputs); - for (int i = 0; i < total_top; i++) { - bool il = parsed.is_lambda_slot(i); - std::string pn = il ? ("@"+std::to_string(i)) : std::to_string(i); - node.inputs.push_back(make_pin("", pn, "", nullptr, il ? FlowPin::Lambda : FlowPin::Input)); - } - if (!cn.args.empty()) { - auto tokens = tokenize_args(cn.args, false); - num_outputs = std::max(1, (int)tokens.size()); - } - } else { - auto info = compute_inline_args(cn.args, nt->inputs); - if (!info.error.empty()) node.error = info.error; - int ref_pins = (info.pin_slots.max_slot >= 0) ? (info.pin_slots.max_slot + 1) : 0; - for (int i = 0; i < ref_pins; i++) { - bool il = info.pin_slots.is_lambda_slot(i); - std::string pn = il ? ("@"+std::to_string(i)) : std::to_string(i); - node.inputs.push_back(make_pin("", pn, "", nullptr, il ? FlowPin::Lambda : FlowPin::Input)); - } - for (int i = info.num_inline_args; i < nt->inputs; i++) { - std::string pn; bool il = false; - if (nt->input_ports && i < nt->inputs) { - pn = nt->input_ports[i].name; - il = (nt->input_ports[i].kind == PortKind::Lambda); - } else pn = std::to_string(i); - node.inputs.push_back(make_pin("", pn, "", nullptr, il ? FlowPin::Lambda : FlowPin::Input)); - } - } - for (int i = 0; i < num_outputs; i++) { - std::string oname = (nt->output_ports && i < nt->outputs) ? nt->output_ports[i].name : ("out" + std::to_string(i)); - node.outputs.push_back(make_pin("", oname, "", nullptr, FlowPin::Output)); - } - for (int i = 0; i < nt->num_nexts; i++) { - std::string bname = (nt->next_ports && i < nt->num_nexts) ? nt->next_ports[i].name : ("bang" + std::to_string(i)); - node.nexts.push_back(make_pin("", bname, "", nullptr, FlowPin::BangNext)); - } - } - node.rebuild_pin_ids(); - active().selected_nodes.insert(id); - break; - } - } + ImGui::End(); // main - // Recreate internal links - for (auto& cl : active().clipboard_links) { - if (cl.from_idx < 0 || cl.from_idx >= (int)new_guids.size()) continue; - if (cl.to_idx < 0 || cl.to_idx >= (int)new_guids.size()) continue; - std::string from_id = new_guids[cl.from_idx] + "." + cl.from_pin_name; - std::string to_id = new_guids[cl.to_idx] + "." + cl.to_pin_name; - active().graph.add_link(from_id, to_id); - } + // Check debounced save for Editor1Pane + if (e1) e1->check_debounced_save(); - // Resolve type-based pins for pasted nodes - resolve_type_based_pins(active().graph); - mark_dirty(); + win_.end_frame(30, 30, 40); } -// --- Run/Stop --- +// --- Toolbar --- void FlowEditorWindow::draw_toolbar() { auto state = build_state_.load(); @@ -2500,114 +354,95 @@ void FlowEditorWindow::draw_toolbar() { bool can_stop = (state == BuildState::Running); if (!can_run) ImGui::BeginDisabled(); - if (ImGui::Button("Run")) { - run_program(false); - } + if (ImGui::Button("Run")) run_program(false); ImGui::SameLine(); - if (ImGui::Button("Run Release")) { - run_program(true); - } + if (ImGui::Button("Run Release")) run_program(true); if (!can_run) ImGui::EndDisabled(); ImGui::SameLine(); - if (!can_stop) ImGui::BeginDisabled(); - if (ImGui::Button("Stop")) { - stop_program(); - } + if (ImGui::Button("Stop")) stop_program(); if (!can_stop) ImGui::EndDisabled(); ImGui::SameLine(); - // Search by node guid + // Search (Editor1 only) ImGui::SameLine(); ImGui::SetNextItemWidth(120); if (ImGui::InputTextWithHint("##search", "Find node...", search_buf_, sizeof(search_buf_), ImGuiInputTextFlags_EnterReturnsTrue)) { - std::string query(search_buf_); - if (!query.empty()) { - for (auto& node : active().graph.nodes) { - if (node.imported || node.shadow) continue; - if (node.guid.find(query) != std::string::npos || - node.display_text().find(query) != std::string::npos) { - center_on_node(node, {last_canvas_w_, last_canvas_h_}); - active().selected_nodes.clear(); - active().selected_nodes.insert(node.id); - break; + if (auto* e1 = dynamic_cast(active().pane.get())) { + std::string query(search_buf_); + if (!query.empty()) { + for (auto& node : e1->graph().nodes) { + if (node.imported || node.shadow) continue; + if (node.guid.find(query) != std::string::npos || + node.display_text().find(query) != std::string::npos) { + e1->center_on_node(node, {last_canvas_w_, last_canvas_h_}); + break; + } } } } } ImGui::SameLine(); - - // Status indicator switch (state) { - case BuildState::Idle: - ImGui::TextDisabled("Idle"); - break; - case BuildState::Building: - ImGui::TextColored({1.0f, 0.8f, 0.0f, 1.0f}, "Building..."); - break; - case BuildState::Running: - ImGui::TextColored({0.0f, 1.0f, 0.0f, 1.0f}, "Running"); - break; - case BuildState::BuildFailed: - ImGui::TextColored({1.0f, 0.2f, 0.2f, 1.0f}, "Build Failed"); - break; + case BuildState::Idle: ImGui::TextDisabled("Idle"); break; + case BuildState::Building: ImGui::TextColored({1.0f, 0.8f, 0.0f, 1.0f}, "Building..."); break; + case BuildState::Running: ImGui::TextColored({0.0f, 1.0f, 0.0f, 1.0f}, "Running"); break; + case BuildState::BuildFailed: ImGui::TextColored({1.0f, 0.2f, 0.2f, 1.0f}, "Build Failed"); break; } } +// --- Run/Stop --- + void FlowEditorWindow::run_program(bool release) { - // Stop existing stop_program(); - - // Wait for any previous build thread if (build_thread_.joinable()) build_thread_.join(); - // Auto-open build log and clear it show_build_log_ = true; { std::lock_guard lock(build_log_mutex_); build_log_.clear(); } - // Auto-save - auto_save(); + // Auto-save via Editor1Pane if applicable + if (auto* e1 = dynamic_cast(active().pane.get())) { + e1->auto_save(); + } - if (active().file_path.empty()) return; + std::string active_path = active().pane ? active().file_path() : ""; + if (active_path.empty()) return; namespace fs = std::filesystem; - // Determine paths — nanoc expects a project folder containing main.atto - fs::path atto_path = fs::absolute(active().file_path); + fs::path atto_path = fs::absolute(active_path); fs::path project_dir = atto_path.parent_path(); std::string source_name = project_dir.filename().string(); fs::path output_dir = project_dir / ".generated" / source_name; - // Find nanoc relative to this exe - fs::path exe_path; + fs::path exe_dir; #ifdef _WIN32 char exe_buf[MAX_PATH]; GetModuleFileNameA(nullptr, exe_buf, MAX_PATH); - exe_path = fs::path(exe_buf).parent_path(); + exe_dir = fs::path(exe_buf).parent_path(); #elif defined(__APPLE__) { uint32_t size = 0; _NSGetExecutablePath(nullptr, &size); std::string buf(size, '\0'); _NSGetExecutablePath(buf.data(), &size); - exe_path = fs::canonical(buf).parent_path(); + exe_dir = fs::canonical(buf).parent_path(); } #else - exe_path = fs::canonical("/proc/self/exe").parent_path(); + exe_dir = fs::canonical("/proc/self/exe").parent_path(); #endif - fs::path attoc_path = exe_path / "attoc.exe"; + fs::path attoc_path = exe_dir / "attoc.exe"; if (!fs::exists(attoc_path)) - attoc_path = exe_path / "attoc"; + attoc_path = exe_dir / "attoc"; - // vcpkg toolchain (Windows only — Linux/macOS use FetchContent via NanoDeps.cmake) std::string tc_str; #ifdef _WIN32 { @@ -2622,7 +457,6 @@ void FlowEditorWindow::run_program(bool release) { } #endif - // Capture paths as strings for the thread std::string attoc_str = attoc_path.string(); std::string atto_str = project_dir.string(); std::string out_str = output_dir.string(); @@ -2640,7 +474,6 @@ void FlowEditorWindow::run_program(bool release) { auto run_cmd = [this](const std::string& cmd) -> int { #ifdef _WIN32 - // cmd.exe needs the entire command wrapped in quotes when args contain quotes std::string full_cmd = "\"" + cmd + " 2>&1\""; FILE* pipe = _popen(full_cmd.c_str(), "r"); #else @@ -2660,18 +493,13 @@ void FlowEditorWindow::run_program(bool release) { #endif }; - // Step 1: nanoc { std::lock_guard lock(build_log_mutex_); build_log_ += "=== Running attoc ===\n"; } std::string cmd1 = "\"" + attoc_str + "\" \"" + atto_str + "\" -o \"" + out_str + "\""; - if (run_cmd(cmd1) != 0) { - build_state_ = BuildState::BuildFailed; - return; - } + if (run_cmd(cmd1) != 0) { build_state_ = BuildState::BuildFailed; return; } - // Step 2: cmake configure (skip if already configured) std::string build_dir = out_str + "/build"; std::string cache_file = build_dir + "/CMakeCache.txt"; { @@ -2684,29 +512,21 @@ void FlowEditorWindow::run_program(bool release) { std::string cmd2 = "cmake -B \"" + build_dir + "\" -S \"" + out_str + "\""; if (!tc_str.empty()) cmd2 += " \"-DCMAKE_TOOLCHAIN_FILE=" + tc_str + "\""; - if (run_cmd(cmd2) != 0) { - build_state_ = BuildState::BuildFailed; - return; - } + if (run_cmd(cmd2) != 0) { build_state_ = BuildState::BuildFailed; return; } } else { std::lock_guard lock(build_log_mutex_); build_log_ += "\n=== CMake Configure (cached) ===\n"; } } - // Step 3: cmake build { std::lock_guard lock(build_log_mutex_); build_log_ += "\n=== CMake Build ===\n"; } std::string config = release ? "Release" : "Debug"; std::string cmd3 = "cmake --build \"" + build_dir + "\" --config " + config + " --parallel"; - if (run_cmd(cmd3) != 0) { - build_state_ = BuildState::BuildFailed; - return; - } + if (run_cmd(cmd3) != 0) { build_state_ = BuildState::BuildFailed; return; } - // Step 4: launch exe #ifdef _WIN32 fs::path exe_path = fs::path(build_dir) / config / (sn + ".exe"); if (!fs::exists(exe_path)) diff --git a/src/attoflow/editor.h b/src/attoflow/editor.h index 684fcc0..1e58daa 100644 --- a/src/attoflow/editor.h +++ b/src/attoflow/editor.h @@ -1,11 +1,10 @@ #pragma once #include "sdl_imgui_window.h" -#include "atto/model.h" -#include "atto/types.h" +#include "tab.h" +#include "editor1.h" #include "editor2.h" #include #include -#include #include #include #include @@ -14,51 +13,6 @@ #include #endif -// Conversion between Vec2 (model) and ImVec2 (UI) -inline ImVec2 to_imvec(Vec2 v) { return {v.x, v.y}; } -inline Vec2 to_vec2(ImVec2 v) { return {v.x, v.y}; } - -// Per-tab state: each open .atto file gets its own TabState -struct TabState { - FlowGraph graph; // legacy (Editor1) - std::shared_ptr editor2; // new editor pane - bool use_editor2 = true; // true = use Editor2Pane, false = legacy - std::string file_path; // absolute path to this .atto file - std::string tab_name; // display name (filename without extension) - bool dirty = false; - - // Canvas - ImVec2 canvas_offset = {0, 0}; - float canvas_zoom = 1.0f; - - // Selection - std::set selected_nodes; - - // Undo/Redo - std::vector undo_stack; - std::vector redo_stack; - - // Type inference - TypePool type_pool; - bool inference_dirty = true; - - // Clipboard - struct ClipboardNode { - NodeTypeID type_id; std::string args; - ImVec2 offset; // relative to centroid - }; - struct ClipboardLink { - int from_idx, to_idx; // indices into clipboard_nodes - std::string from_pin_name, to_pin_name; - }; - std::vector clipboard_nodes; - std::vector clipboard_links; - - // Highlight animation - int highlight_node_id = -1; - float highlight_timer = 0.0f; -}; - class FlowEditorWindow { public: bool init(const std::string& project_dir = ""); @@ -69,7 +23,6 @@ class FlowEditorWindow { void draw(); SdlImGuiWindow& sdl_window() { return win_; } - FlowGraph& graph() { return active().graph; } // Tab management TabState& active() { return tabs_[active_tab_]; } @@ -83,86 +36,13 @@ class FlowEditorWindow { // Project std::string project_dir_; - std::vector project_files_; // cached .atto filenames + std::vector project_files_; float file_panel_width_ = 200.0f; // Tabs std::vector tabs_; int active_tab_ = 0; - // Per-tab helpers (operate on active tab) - void mark_dirty(); - void auto_save(); - void push_undo(); - void undo(); - void redo(); - void copy_selection(); - void paste_at(ImVec2 canvas_pos); - - // Debounced save - void schedule_save(); - double save_deadline_ = 0; // 0 = no pending save - void check_debounced_save(); - - // Interaction state (global — always applies to active tab) - int dragging_node_ = -1; - bool dragging_selection_ = false; - std::string dragging_link_from_pin_; - bool dragging_link_from_output_ = true; // true if drag started from output-like pin - ImVec2 dragging_link_start_; - bool canvas_dragging_ = false; - ImVec2 canvas_drag_start_; - - // Grabbed links - struct GrabbedLink { std::string from_pin; std::string to_pin; }; - std::vector grabbed_links_; - std::string grabbed_pin_; - bool grab_is_output_ = false; - bool grab_pending_ = false; - ImVec2 grab_start_; - - // Box selection - bool box_selecting_ = false; - ImVec2 box_select_start_; - - // Node name editing - int editing_node_ = -1; - std::string edit_buf_; - bool edit_just_opened_ = false; - bool edit_cursor_to_end_ = false; - bool creating_new_node_ = false; - ImVec2 new_node_pos_; - - // Link/wire name editing - int editing_link_ = -1; - std::string link_edit_buf_; - bool link_edit_just_opened_ = false; - - // Shadow pin filtering (rebuilt each frame before drawing) - std::set shadow_connected_pins_; // pin IDs connected from shadow nodes - - // Drawing helpers - ImVec2 canvas_to_screen(ImVec2 p, ImVec2 canvas_origin) const; - ImVec2 screen_to_canvas(ImVec2 p, ImVec2 canvas_origin) const; - ImVec2 get_pin_pos(const FlowNode& node, const FlowPin& pin, ImVec2 canvas_origin) const; - void draw_node(ImDrawList* dl, FlowNode& node, ImVec2 canvas_origin); - void draw_link(ImDrawList* dl, const FlowLink& link, ImVec2 canvas_origin); - - // Hit testing - struct PinHit { int node_id; std::string pin_id; FlowPin::Direction dir; }; - PinHit hit_test_pin(ImVec2 screen_pos, ImVec2 canvas_origin, float radius = 8.0f) const; - int hit_test_link(ImVec2 screen_pos, ImVec2 canvas_origin, float threshold = 6.0f) const; - - // Validation & type inference - void validate_nodes(); - void run_type_inference(); - - // Navigation - void center_on_node(const FlowNode& node, ImVec2 canvas_size); - - // Viewport sync - void sync_viewport(TabState& tab); - // Panel sizes float side_panel_width_ = 200.0f; float bottom_panel_height_ = 250.0f; diff --git a/src/attoflow/editor1.cpp b/src/attoflow/editor1.cpp new file mode 100644 index 0000000..b3a7fdf --- /dev/null +++ b/src/attoflow/editor1.cpp @@ -0,0 +1,2010 @@ +#include "editor1.h" +#include "atto/args.h" +#include "atto/expr.h" +#include "atto/inference.h" +#include "atto/serial.h" +#include "atto/shadow.h" +#include "atto/types.h" +#include "atto/node_types.h" +#include +#include +#include +#include +#include +#include + +// --- Constants --- +static constexpr float NODE_ROUNDING = 4.0f; +static constexpr float PIN_RADIUS = 5.0f; +static constexpr float PIN_SPACING = 20.0f; +static constexpr float NODE_HEIGHT = 31.0f; +static constexpr float NODE_MIN_WIDTH = 80.0f; +static constexpr float GRID_SIZE = 32.0f; + +static constexpr ImU32 COL_BG = IM_COL32(30, 30, 40, 255); +static constexpr ImU32 COL_GRID = IM_COL32(50, 50, 60, 255); +static constexpr ImU32 COL_NODE_BG = IM_COL32(60, 60, 90, 230); +static constexpr ImU32 COL_PIN_IN = IM_COL32(100, 200, 100, 255); +static constexpr ImU32 COL_PIN_OUT = IM_COL32(200, 100, 100, 255); +static constexpr ImU32 COL_PIN_HOVER = IM_COL32(255, 255, 255, 255); +static constexpr ImU32 COL_LINK = IM_COL32(200, 200, 100, 200); +static constexpr ImU32 COL_LINK_DRAG = IM_COL32(255, 255, 150, 200); + +// --- Static helpers --- + +#include "atto/type_utils.h" + +// Look up port description for a pin on a node. +// Returns {port_name, port_desc} or {"", ""} if not found. +static std::pair get_port_desc(const FlowNode& node, const FlowPin& pin) { + if (node.lambda_grab.id == pin.id) return {"as_lambda", "pass as lambda"}; + if (node.bang_pin.id == pin.id) return {"bang", "bang connector"}; + + auto* nt = find_node_type(node.type_id); + + auto find_bang = [&](const auto& pins, const PortDesc* descs, int count) -> std::pair { + int idx = 0; + for (auto& p : pins) { + if (p->id == pin.id) { + if (descs && idx < count) return {descs[idx].name, descs[idx].desc}; + return {pin.name, ""}; + } + idx++; + } + return {"", ""}; + }; + + if (nt) { + auto r = find_bang(node.triggers, nt->trigger_ports, nt->num_triggers); + if (!r.first.empty()) return r; + r = find_bang(node.nexts, nt->next_ports, nt->num_nexts); + if (!r.first.empty()) return r; + r = find_bang(node.outputs, nt->output_ports, nt->outputs); + if (!r.first.empty()) return r; + } + + for (int i = 0; i < (int)node.inputs.size(); i++) { + if (node.inputs[i]->id != pin.id) continue; + for (auto& expr : node.parsed_exprs) { + if (!expr) continue; + struct Finder { + int target_idx; std::string result; + void walk(const ExprPtr& e) { + if (!e || !result.empty()) return; + if (e->kind == ExprKind::PinRef && e->pin_ref.index == target_idx && !e->pin_ref.name.empty()) + result = e->pin_ref.name; + for (auto& c : e->children) walk(c); + } + }; + int pin_idx = -1; + try { pin_idx = std::stoi(pin.name); } catch (...) {} + if (pin_idx >= 0) { + Finder f{pin_idx, {}}; + f.walk(expr); + if (!f.result.empty()) return {f.result, ""}; + } + } + return {pin.name, ""}; + } + + return {pin.name, ""}; +} + +// Get a display-friendly name for a node +static std::string node_display_name(const FlowNode& node) { + return node.display_text(); +} + +// Build "display_name.port_name" label for a pin +static std::string pin_label(const FlowNode& node, const FlowPin& pin) { + auto [port_name, _] = get_port_desc(node, pin); + return node_display_name(node) + "." + port_name; +} + +static float dist2(ImVec2 a, ImVec2 b) { + float dx = a.x - b.x, dy = a.y - b.y; + return dx * dx + dy * dy; +} + +static float point_to_bezier_dist(ImVec2 p, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3) { + float min_d2 = 1e18f; + for (int i = 0; i <= 20; i++) { + float t = i / 20.0f; + float u = 1.0f - t; + float x = u*u*u*p0.x + 3*u*u*t*p1.x + 3*u*t*t*p2.x + t*t*t*p3.x; + float y = u*u*u*p0.y + 3*u*u*t*p1.y + 3*u*t*t*p2.y + t*t*t*p3.y; + float dx = p.x - x, dy = p.y - y; + float d2 = dx*dx + dy*dy; + if (d2 < min_d2) min_d2 = d2; + } + return std::sqrt(min_d2); +} + +enum class PinShape { Square, Signal, LambdaDown, LambdaLeft }; + +static void draw_pin(ImDrawList* dl, ImVec2 pos, float r, ImU32 col, PinShape shape, float zoom) { + switch (shape) { + case PinShape::Signal: + dl->AddCircleFilled(pos, r, col); + { + float font_sz = r * 1.6f; + if (font_sz > 3.0f) { + ImVec2 ts = ImGui::CalcTextSize("~"); + float scale = font_sz / ImGui::GetFontSize(); + dl->AddText(nullptr, font_sz, + {pos.x - ts.x * scale * 0.5f, pos.y - ts.y * scale * 0.5f}, + IM_COL32(30, 30, 40, 255), "~"); + } + } + break; + case PinShape::LambdaDown: + dl->AddTriangleFilled( + {pos.x - r, pos.y - r}, + {pos.x + r, pos.y - r}, + {pos.x, pos.y + r}, + col); + break; + case PinShape::LambdaLeft: + dl->AddTriangleFilled( + {pos.x + r, pos.y - r}, + {pos.x - r, pos.y}, + {pos.x + r, pos.y + r}, + col); + break; + case PinShape::Square: + default: + dl->AddRectFilled({pos.x - r, pos.y - r}, {pos.x + r, pos.y + r}, col); + break; + } +} + +static void draw_pin_highlight(ImDrawList* dl, ImVec2 pos, float r, ImU32 col, PinShape shape, float zoom) { + float o = 2 * zoom; + switch (shape) { + case PinShape::Signal: + dl->AddCircle(pos, r + o, col, 0, 2.0f); + break; + case PinShape::LambdaDown: + dl->AddTriangle( + {pos.x - r - o, pos.y - r - o}, + {pos.x + r + o, pos.y - r - o}, + {pos.x, pos.y + r + o}, + col, 2.0f); + break; + case PinShape::LambdaLeft: + dl->AddTriangle( + {pos.x + r + o, pos.y - r - o}, + {pos.x - r - o, pos.y}, + {pos.x + r + o, pos.y + r + o}, + col, 2.0f); + break; + case PinShape::Square: + default: + dl->AddRect({pos.x - r - o, pos.y - r - o}, {pos.x + r + o, pos.y + r + o}, col, 0, 0, 2.0f); + break; + } +} + +static void draw_vbezier(ImDrawList* dl, ImVec2 from, ImVec2 to, ImU32 col, float thickness, float zoom) { + float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * zoom); + dl->AddBezierCubic(from, {from.x, from.y + dy}, {to.x, to.y - dy}, to, col, thickness * zoom); +} + +static ImVec2 bezier_sample(ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3, float t) { + float u = 1.0f - t; + float uu = u * u, uuu = uu * u; + float tt = t * t, ttt = tt * t; + return {uuu*p0.x + 3*uu*t*p1.x + 3*u*tt*p2.x + ttt*p3.x, + uuu*p0.y + 3*uu*t*p1.y + 3*u*tt*p2.y + ttt*p3.y}; +} + +static void draw_dashed_bezier(ImDrawList* dl, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3, + ImU32 col, float thickness, float dash_len, float gap_len) { + const int N = 128; + ImVec2 pts[N + 1]; + float arc[N + 1]; + pts[0] = p0; arc[0] = 0; + for (int i = 1; i <= N; i++) { + pts[i] = bezier_sample(p0, p1, p2, p3, (float)i / N); + float dx = pts[i].x - pts[i-1].x, dy = pts[i].y - pts[i-1].y; + arc[i] = arc[i-1] + sqrtf(dx*dx + dy*dy); + } + float total = arc[N]; + if (total < 1.0f) return; + float cycle = dash_len + gap_len; + + auto lerp_at = [&](float d) -> ImVec2 { + if (d <= 0) return pts[0]; + if (d >= total) return pts[N]; + int lo = 0, hi = N; + while (lo < hi - 1) { int mid = (lo+hi)/2; if (arc[mid] < d) lo = mid; else hi = mid; } + float seg_len = arc[hi] - arc[lo]; + float t = (seg_len > 0) ? (d - arc[lo]) / seg_len : 0; + return {pts[lo].x + t * (pts[hi].x - pts[lo].x), + pts[lo].y + t * (pts[hi].y - pts[lo].y)}; + }; + + float d = 0; + while (d < total) { + float d_end = std::min(d + dash_len, total); + ImVec2 prev = lerp_at(d); + float step = 3.0f; + for (float dd = d + step; dd <= d_end; dd += step) { + ImVec2 cur = lerp_at(dd); + dl->AddLine(prev, cur, col, thickness); + prev = cur; + } + ImVec2 end = lerp_at(d_end); + dl->AddLine(prev, end, col, thickness); + d += cycle; + } +} + +static void draw_dashed_vbezier(ImDrawList* dl, ImVec2 from, ImVec2 to, ImU32 col, float thickness, float zoom) { + float dy = std::max(std::abs(to.y - from.y) * 0.5f, 30.0f * zoom); + draw_dashed_bezier(dl, from, {from.x, from.y + dy}, {to.x, to.y - dy}, to, + col, thickness * zoom, 8.0f * zoom, 4.0f * zoom); +} + +// ============================================================ +// Editor1Pane methods +// ============================================================ + +bool Editor1Pane::load(const std::string& path) { + namespace fs = std::filesystem; + std::string abs_path = fs::absolute(path).string(); + file_path_ = abs_path; + tab_name_ = fs::path(path).stem().string(); + + if (fs::exists(abs_path)) { + if (!load_atto(abs_path, graph_)) + return false; + } + + if (graph_.has_viewport) { + canvas_offset_ = {graph_.viewport_x, graph_.viewport_y}; + canvas_zoom_ = graph_.viewport_zoom; + } + inference_dirty_ = true; + return true; +} + +void Editor1Pane::mark_dirty() { + push_undo(); + dirty_ = true; + inference_dirty_ = true; + schedule_save(); +} + +void Editor1Pane::push_undo() { + undo_stack_.push_back(save_atto_string(graph_)); + redo_stack_.clear(); + if (undo_stack_.size() > 200) undo_stack_.erase(undo_stack_.begin()); +} + +void Editor1Pane::undo() { + if (undo_stack_.empty()) return; + redo_stack_.push_back(save_atto_string(graph_)); + load_atto_string(undo_stack_.back(), graph_); + undo_stack_.pop_back(); + dirty_ = true; +} + +void Editor1Pane::redo() { + if (redo_stack_.empty()) return; + undo_stack_.push_back(save_atto_string(graph_)); + load_atto_string(redo_stack_.back(), graph_); + redo_stack_.pop_back(); + dirty_ = true; +} + +void Editor1Pane::schedule_save() { + dirty_ = true; + save_deadline_ = ImGui::GetTime() + 0.5; +} + +void Editor1Pane::check_debounced_save() { + if (save_deadline_ > 0 && ImGui::GetTime() >= save_deadline_) { + save_deadline_ = 0; + auto_save(); + } +} + +void Editor1Pane::sync_viewport() { + graph_.viewport_x = canvas_offset_.x; + graph_.viewport_y = canvas_offset_.y; + graph_.viewport_zoom = canvas_zoom_; +} + +void Editor1Pane::auto_save() { + if (dirty_ && !file_path_.empty()) { + sync_viewport(); + save_atto(file_path_, graph_); + dirty_ = false; + } +} + +ImVec2 Editor1Pane::canvas_to_screen(ImVec2 p, ImVec2 origin) const { + return {origin.x + (p.x + canvas_offset_.x) * canvas_zoom_, + origin.y + (p.y + canvas_offset_.y) * canvas_zoom_}; +} + +ImVec2 Editor1Pane::screen_to_canvas(ImVec2 p, ImVec2 origin) const { + return {(p.x - origin.x) / canvas_zoom_ - canvas_offset_.x, + (p.y - origin.y) / canvas_zoom_ - canvas_offset_.y}; +} + +ImVec2 Editor1Pane::get_pin_pos(const FlowNode& node, const FlowPin& pin, ImVec2 origin) const { + if (pin.direction == FlowPin::LambdaGrab) { + float x = node.position.x; + float y = node.position.y + node.size.y * 0.5f; + return canvas_to_screen({x, y}, origin); + } + + if (pin.id == node.bang_pin.id && pin.name == "bang") { + float x = node.position.x + node.size.x; + float y = node.position.y + node.size.y * 0.5f; + return canvas_to_screen({x, y}, origin); + } + + if (pin.direction == FlowPin::BangTrigger) { + int idx = 0; + for (auto& p : node.triggers) { if (p->id == pin.id) break; idx++; } + float x = node.position.x + PIN_SPACING * (idx + 0.5f); + float y = node.position.y; + return canvas_to_screen({x, y}, origin); + } + + if (pin.direction == FlowPin::Input || pin.direction == FlowPin::Lambda) { + int bang_offset = (int)node.triggers.size(); + int slot = 0; + for (auto& p : node.inputs) { + if (p->id == pin.id) break; + if (!shadow_connected_pins_.count(p->id)) slot++; + } + float x = node.position.x + PIN_SPACING * (bang_offset + slot + 0.5f); + float y = node.position.y; + return canvas_to_screen({x, y}, origin); + } + + if (pin.direction == FlowPin::BangNext) { + int idx = 0; + for (auto& p : node.nexts) { if (p->id == pin.id) break; idx++; } + float x = node.position.x + PIN_SPACING * (idx + 0.5f); + float y = node.position.y + node.size.y; + return canvas_to_screen({x, y}, origin); + } + + int offset = (int)node.nexts.size(); + int idx = 0; + for (auto& p : node.outputs) { if (p->id == pin.id) break; idx++; } + float x = node.position.x + PIN_SPACING * (offset + idx + 0.5f); + float y = node.position.y + node.size.y; + return canvas_to_screen({x, y}, origin); +} + +Editor1Pane::PinHit Editor1Pane::hit_test_pin(ImVec2 sp, ImVec2 co, float radius) const { + float r2 = radius * radius * canvas_zoom_ * canvas_zoom_; + for (auto& node : graph_.nodes) { + if (node.imported || node.shadow) continue; + for (auto& pin : node.triggers) + if (dist2(sp, get_pin_pos(node, *pin, co)) < r2) + return {node.id, pin->id, FlowPin::BangTrigger}; + for (auto& pin : node.inputs) + if (dist2(sp, get_pin_pos(node, *pin, co)) < r2) + return {node.id, pin->id, pin->direction}; + for (auto& pin : node.outputs) + if (dist2(sp, get_pin_pos(node, *pin, co)) < r2) + return {node.id, pin->id, FlowPin::Output}; + for (auto& pin : node.nexts) + if (dist2(sp, get_pin_pos(node, *pin, co)) < r2) + return {node.id, pin->id, FlowPin::BangNext}; + if (!node.lambda_grab.id.empty() && dist2(sp, get_pin_pos(node, node.lambda_grab, co)) < r2) { + auto* nt_hit = find_node_type(node.type_id); + if (nt_hit && nt_hit->has_lambda) + return {node.id, node.lambda_grab.id, FlowPin::LambdaGrab}; + } + if (!node.bang_pin.id.empty() && dist2(sp, get_pin_pos(node, node.bang_pin, co)) < r2) { + auto* nt_hit = find_node_type(node.type_id); + bool hidden = (nt_hit && (nt_hit->is_event || nt_hit->no_post_bang)); + if (!hidden) return {node.id, node.bang_pin.id, FlowPin::BangNext}; + } + } + return {-1, "", FlowPin::Input}; +} + +int Editor1Pane::hit_test_link(ImVec2 sp, ImVec2 co, float threshold) const { + for (auto& link : graph_.links) { + ImVec2 fp = {}, tp = {}; + bool ff = false, ft = false; + bool from_grab = false, from_bang_pin = false, to_lambda = false; + for (auto& n : graph_.nodes) { + for (auto& p : n.outputs) if (p->id == link.from_pin) { fp = get_pin_pos(n, *p, co); ff = true; } + for (auto& p : n.nexts) if (p->id == link.from_pin) { fp = get_pin_pos(n, *p, co); ff = true; } + for (auto& p : n.triggers) if (p->id == link.from_pin) { fp = get_pin_pos(n, *p, co); ff = true; } + if (n.lambda_grab.id == link.from_pin) { fp = get_pin_pos(n, n.lambda_grab, co); ff = true; from_grab = true; } + for (auto& p : n.triggers) if (p->id == link.to_pin) { tp = get_pin_pos(n, *p, co); ft = true; } + for (auto& p : n.inputs) if (p->id == link.to_pin) { tp = get_pin_pos(n, *p, co); ft = true; if (p->direction == FlowPin::Lambda) to_lambda = true; } + if (n.bang_pin.id == link.from_pin) { fp = get_pin_pos(n, n.bang_pin, co); ff = true; from_bang_pin = true; } + } + if (!ff || !ft) continue; + float d; + if (from_grab) { + float dx = std::max(std::abs(tp.x - fp.x) * 0.5f, 30.0f * canvas_zoom_); + float dy = std::max(std::abs(tp.y - fp.y) * 0.5f, 30.0f * canvas_zoom_); + d = point_to_bezier_dist(sp, fp, {fp.x - dx, fp.y}, {tp.x, tp.y - dy}, tp); + } else if (from_bang_pin) { + float dx = std::max(std::abs(tp.x - fp.x) * 0.5f, 30.0f * canvas_zoom_); + float dy_hit = std::max(std::abs(tp.y - fp.y) * 0.5f, 30.0f * canvas_zoom_); + d = point_to_bezier_dist(sp, fp, {fp.x + dx, fp.y}, {tp.x, tp.y - dy_hit}, tp); + } else { + float dy = std::max(std::abs(tp.y - fp.y) * 0.5f, 30.0f * canvas_zoom_); + d = point_to_bezier_dist(sp, fp, {fp.x, fp.y + dy}, {tp.x, tp.y - dy}, tp); + } + if (d < threshold * canvas_zoom_) return link.id; + } + return -1; +} + +void Editor1Pane::draw_node(ImDrawList* dl, FlowNode& node, ImVec2 origin) { + bool is_label = (node.type_id == NodeTypeID::Label); + + int visible_inputs = 0; + for (auto& pin : node.inputs) + if (!shadow_connected_pins_.count(pin->id)) visible_inputs++; + int top_pins = (int)node.triggers.size() + visible_inputs; + int bottom_pins = (int)(node.nexts.size() + node.outputs.size()); + int max_pins = std::max(top_pins, bottom_pins); + float pin_w = (float)(max_pins + 1) * PIN_SPACING; + + std::string display_text; + if (is_label) { + display_text = node.args.empty() ? "(label)" : node.args; + } else { + display_text = node.display_text(); + } + float font_scale = 17.0f / ImGui::GetFontSize(); + ImVec2 ts = ImGui::CalcTextSize(display_text.c_str()); + float text_w = ts.x * font_scale + 16.0f; + + float needed_w = std::max({pin_w, text_w, NODE_MIN_WIDTH}); + node.size = {needed_w, NODE_HEIGHT}; + + ImVec2 tl = canvas_to_screen(to_imvec(node.position), origin); + ImVec2 br = canvas_to_screen({node.position.x + node.size.x, + node.position.y + node.size.y}, origin); + + if (is_label) { + float font_size = 17.0f * canvas_zoom_; + if (font_size > 6.0f && editing_node_ != node.id) { + const char* display = node.args.empty() ? "(label)" : node.args.c_str(); + ImU32 col = node.args.empty() ? IM_COL32(100, 100, 100, 180) : IM_COL32(255, 255, 255, 255); + dl->AddText(nullptr, font_size, + {tl.x + 2 * canvas_zoom_, tl.y + (br.y - tl.y - font_size) * 0.5f}, + col, display); + } + } else { + ImU32 bg = node.error.empty() ? COL_NODE_BG : IM_COL32(120, 30, 30, 230); + ImU32 border = node.error.empty() ? IM_COL32(100, 100, 150, 255) : IM_COL32(200, 60, 60, 255); + dl->AddRectFilled(tl, br, bg, NODE_ROUNDING * canvas_zoom_); + + if (highlight_node_id_ == node.id && highlight_timer_ > 0.0f) { + float blink = std::sin(highlight_timer_ * 6.0f) * 0.5f + 0.5f; + int a = (int)(blink * 140.0f); + dl->AddRectFilled(tl, br, IM_COL32(180, 160, 40, a), NODE_ROUNDING * canvas_zoom_); + } + + dl->AddRect(tl, br, border, NODE_ROUNDING * canvas_zoom_); + + if (selected_nodes_.count(node.id)) { + dl->AddRect({tl.x - 2*canvas_zoom_, tl.y - 2*canvas_zoom_}, + {br.x + 2*canvas_zoom_, br.y + 2*canvas_zoom_}, + IM_COL32(100, 180, 255, 200), NODE_ROUNDING * canvas_zoom_, 0, 2.0f * canvas_zoom_); + } + + float font_size = 17.0f * canvas_zoom_; + if (font_size > 6.0f && editing_node_ != node.id) { + std::string text = node.display_text(); + float scale = font_size / ImGui::GetFontSize(); + ImVec2 text_sz = ImGui::CalcTextSize(text.c_str()); + float tw = text_sz.x * scale; + float cx = (tl.x + br.x) * 0.5f - tw * 0.5f; + float cy = tl.y + (br.y - tl.y - font_size) * 0.5f; + dl->AddText(nullptr, font_size, {cx, cy}, IM_COL32(220, 220, 220, 255), text.c_str()); + } + } + + auto* nt = find_node_type(node.type_id); + bool is_event = nt && nt->is_event; + + PinShape io_shape = PinShape::Signal; + float pr = PIN_RADIUS * canvas_zoom_; + { + for (auto& pin : node.triggers) { + ImVec2 pp = get_pin_pos(node, *pin, origin); + draw_pin(dl, pp, pr, IM_COL32(255, 200, 80, 255), PinShape::Square, canvas_zoom_); + } + for (auto& pin : node.inputs) { + if (shadow_connected_pins_.count(pin->id)) continue; + ImVec2 pp = get_pin_pos(node, *pin, origin); + if (pin->direction == FlowPin::Lambda) + draw_pin(dl, pp, pr, IM_COL32(180, 130, 255, 255), PinShape::LambdaDown, canvas_zoom_); + else + draw_pin(dl, pp, pr, COL_PIN_IN, io_shape, canvas_zoom_); + } + for (auto& pin : node.nexts) { + ImVec2 pp = get_pin_pos(node, *pin, origin); + draw_pin(dl, pp, pr, IM_COL32(255, 200, 80, 255), PinShape::Square, canvas_zoom_); + } + for (auto& pin : node.outputs) { + ImVec2 pp = get_pin_pos(node, *pin, origin); + draw_pin(dl, pp, pr, COL_PIN_OUT, io_shape, canvas_zoom_); + } + bool show_lambda = nt && nt->has_lambda; + if (!node.lambda_grab.id.empty() && show_lambda) { + ImVec2 pp = get_pin_pos(node, node.lambda_grab, origin); + draw_pin(dl, pp, pr, IM_COL32(180, 130, 255, 150), PinShape::LambdaLeft, canvas_zoom_); + } + bool no_post_bang = nt && nt->no_post_bang; + if (!node.bang_pin.id.empty() && !is_event && !no_post_bang) { + ImVec2 pp = get_pin_pos(node, node.bang_pin, origin); + draw_pin(dl, pp, pr * 0.7f, IM_COL32(255, 200, 80, 255), PinShape::Square, canvas_zoom_); + } + } +} + +void Editor1Pane::draw_link(ImDrawList* dl, const FlowLink& link, ImVec2 origin) { + ImVec2 fp = {}, tp = {}; + bool ff = false, ft = false; + bool to_lambda = false; + bool from_grab = false; + bool from_bang_pin = false; + FlowPin* from_pin_ptr = nullptr; + FlowPin* to_pin_ptr = nullptr; + for (auto& n : graph_.nodes) { + for (auto& p : n.outputs) if (p->id == link.from_pin) { fp = get_pin_pos(n, *p, origin); ff = true; from_pin_ptr = p.get(); } + for (auto& p : n.nexts) if (p->id == link.from_pin) { fp = get_pin_pos(n, *p, origin); ff = true; from_pin_ptr = p.get(); } + for (auto& p : n.triggers) if (p->id == link.from_pin) { fp = get_pin_pos(n, *p, origin); ff = true; from_pin_ptr = p.get(); } + if (n.lambda_grab.id == link.from_pin) { fp = get_pin_pos(n, n.lambda_grab, origin); ff = true; from_grab = true; from_pin_ptr = &n.lambda_grab; } + if (n.bang_pin.id == link.from_pin) { fp = get_pin_pos(n, n.bang_pin, origin); ff = true; from_bang_pin = true; from_pin_ptr = &n.bang_pin; } + for (auto& p : n.triggers) if (p->id == link.to_pin) { tp = get_pin_pos(n, *p, origin); ft = true; to_pin_ptr = p.get(); } + for (auto& p : n.inputs) if (p->id == link.to_pin) { tp = get_pin_pos(n, *p, origin); ft = true; to_pin_ptr = p.get(); if (p->direction == FlowPin::Lambda) to_lambda = true; } + } + if (!ff || !ft) return; + + bool type_error = !link.error.empty(); + if (!type_error && from_pin_ptr && to_pin_ptr && + from_pin_ptr->resolved_type && to_pin_ptr->resolved_type && + !from_pin_ptr->resolved_type->is_generic && !to_pin_ptr->resolved_type->is_generic) { + type_error = !types_compatible(from_pin_ptr->resolved_type, to_pin_ptr->resolved_type); + } + + bool from_trigger = from_pin_ptr && from_pin_ptr->direction == FlowPin::BangTrigger; + + ImU32 col_error = IM_COL32(255, 60, 60, 220); + bool named = !link.net_name.empty() && !link.auto_wire; + + auto dim = [](ImU32 c) -> ImU32 { + return (c & 0x00FFFFFF) | (((c >> 24) * 100 / 255) << 24); + }; + if (named) col_error = dim(col_error); + + auto wire_col = [&](ImU32 c) { return named ? dim(c) : c; }; + + if (from_trigger) { + float dy = std::max(std::abs(tp.y - fp.y) * 0.5f, 40.0f * canvas_zoom_); + ImU32 col = type_error ? col_error : wire_col(IM_COL32(255, 200, 80, 200)); + float th = 2.5f * canvas_zoom_; + if (named) + draw_dashed_bezier(dl, fp, {fp.x, fp.y - dy}, {tp.x, tp.y - dy}, tp, col, th, 8.0f * canvas_zoom_, 4.0f * canvas_zoom_); + else + dl->AddBezierCubic(fp, {fp.x, fp.y - dy}, {tp.x, tp.y - dy}, tp, col, th); + } else if (from_grab) { + float dx = std::max(std::abs(tp.x - fp.x) * 0.5f, 30.0f * canvas_zoom_); + float dy = std::max(std::abs(tp.y - fp.y) * 0.5f, 30.0f * canvas_zoom_); + ImU32 col = type_error ? col_error : wire_col(IM_COL32(180, 130, 255, 200)); + float th = 2.5f * canvas_zoom_; + if (named) + draw_dashed_bezier(dl, fp, {fp.x - dx, fp.y}, {tp.x, tp.y - dy}, tp, col, th, 8.0f * canvas_zoom_, 4.0f * canvas_zoom_); + else + dl->AddBezierCubic(fp, {fp.x - dx, fp.y}, {tp.x, tp.y - dy}, tp, col, th); + } else if (from_bang_pin) { + float dx = std::max(std::abs(tp.x - fp.x) * 0.5f, 30.0f * canvas_zoom_); + float dy = std::max(std::abs(tp.y - fp.y) * 0.5f, 30.0f * canvas_zoom_); + ImU32 col = type_error ? col_error : wire_col(IM_COL32(255, 200, 80, 200)); + float th = 2.5f * canvas_zoom_; + if (named) + draw_dashed_bezier(dl, fp, {fp.x + dx, fp.y}, {tp.x, tp.y - dy}, tp, col, th, 8.0f * canvas_zoom_, 4.0f * canvas_zoom_); + else + dl->AddBezierCubic(fp, {fp.x + dx, fp.y}, {tp.x, tp.y - dy}, tp, col, th); + } else if (to_lambda) { + ImU32 col = type_error ? col_error : wire_col(IM_COL32(180, 130, 255, 200)); + if (named) + draw_dashed_vbezier(dl, fp, tp, col, 2.5f, canvas_zoom_); + else + draw_vbezier(dl, fp, tp, col, 2.5f, canvas_zoom_); + } else { + ImU32 col = type_error ? col_error : wire_col(COL_LINK); + if (named) + draw_dashed_vbezier(dl, fp, tp, col, 2.5f, canvas_zoom_); + else + draw_vbezier(dl, fp, tp, col, 2.5f, canvas_zoom_); + } + + if (!link.net_name.empty() && !link.auto_wire) { + float font_size = ImGui::GetFontSize() * canvas_zoom_ * 0.8f; + if (font_size > 5.0f) { + ImVec2 mid = {(fp.x + tp.x) * 0.5f, (fp.y + tp.y) * 0.5f}; + ImVec2 text_sz = ImGui::CalcTextSize(link.net_name.c_str()); + float tw = text_sz.x * (font_size / ImGui::GetFontSize()); + float th = text_sz.y * (font_size / ImGui::GetFontSize()); + float cx = mid.x - tw * 0.5f; + float cy = mid.y - th * 0.5f; + dl->AddRectFilled({cx - 3, cy - 1}, {cx + tw + 3, cy + th + 1}, + IM_COL32(30, 30, 40, 200), 3.0f); + dl->AddText(nullptr, font_size, {cx, cy}, IM_COL32(180, 220, 255, 255), link.net_name.c_str()); + } + } +} + +void Editor1Pane::validate_nodes() { + resolve_type_based_pins(graph_); + + TypeRegistry registry; + for (auto& node : graph_.nodes) { + if (node.type_id == NodeTypeID::DeclType) { + auto tokens = tokenize_args(node.args, false); + if (tokens.size() >= 2) { + std::string type_name = tokens[0]; + std::string def; + for (size_t i = 1; i < tokens.size(); i++) { + if (!def.empty()) def += " "; + def += tokens[i]; + } + int decl_class = classify_decl_type(tokens); + if (decl_class == 0 || decl_class == 1) { + registry.register_type(type_name, def); + } else { + registry.register_type(type_name, "void"); + } + } + } + } + + registry.resolve_all(); + + for (auto& node : graph_.nodes) { + node.error.clear(); + + auto* nt = find_node_type(node.type_id); + if (!nt) { + node.error = "Unknown node type: " + std::string(node_type_str(node.type_id)); + continue; + } + + for (auto& other : graph_.nodes) { + if (&other != &node && other.guid == node.guid) { + node.error = "Duplicate guid: " + node.guid; + break; + } + } + if (!node.error.empty()) continue; + + if (node.type_id == NodeTypeID::DeclType) { + auto tokens = tokenize_args(node.args, false); + if (tokens.empty()) { + node.error = "decl_type requires a type name"; + continue; + } + std::string type_name = tokens[0]; + if (!type_name.empty() && type_name[0] == '$') { + node.error = "Type name should not start with $"; + continue; + } + + auto err_it = registry.errors.find(type_name); + if (err_it != registry.errors.end()) { + node.error = err_it->second; + continue; + } + + int decl_class_v = classify_decl_type(tokens); + if (decl_class_v == 2) { + bool has_any_field = false; + for (size_t i = 1; i < tokens.size(); i++) { + if (tokens[i].find(':') != std::string::npos) { has_any_field = true; break; } + } + if (!has_any_field) { + node.error = "Struct type '" + type_name + "' must have at least one field (name:type)"; + continue; + } + } + + for (size_t i = 1; i < tokens.size(); i++) { + auto& tok = tokens[i]; + if (tok == "->" || tok[0] == '(') continue; + auto colon = tok.find(':'); + if (colon != std::string::npos) { + std::string field_type = tok.substr(colon + 1); + std::string err; + if (!registry.validate_type(field_type, err)) { + node.error = "Field '" + tok.substr(0, colon) + "': " + err; + break; + } + } + } + } + + if (node.type_id == NodeTypeID::DeclVar) { + auto tokens = tokenize_args(node.args, false); + if (tokens.size() < 2) { + node.error = "decl_var requires: name type"; + continue; + } + if (!tokens[0].empty() && tokens[0][0] == '$') { + node.error = "Variable name should not start with $ in declarations"; + continue; + } + std::string err; + if (!registry.validate_type(tokens[1], err)) { + node.error = "Invalid type: " + err; + } + } + + if (node.type_id == NodeTypeID::New) { + auto tokens = tokenize_args(node.args, false); + if (tokens.empty()) { + node.error = "new requires a type name"; + continue; + } + if (registry.type_defs.count(tokens[0]) == 0) { + node.error = "Unknown type: " + tokens[0]; + } + } + + if (node.type_id == NodeTypeID::EventBang) { + auto tokens = tokenize_args(node.args, false); + if (tokens.empty()) { + node.error = "event! requires an event name (e.g. ~my_event)"; + continue; + } + if (tokens[0].empty() || tokens[0][0] != '~') { + node.error = "Event name must start with ~ (e.g. ~" + tokens[0] + ")"; + continue; + } + auto* event_decl = find_event_node(graph_, tokens[0]); + if (!event_decl) { + node.error = "Unknown event: " + tokens[0]; + continue; + } + auto ev_tokens = tokenize_args(event_decl->args, false); + bool found_arrow = false; + std::string ret_type; + for (size_t i = 1; i < ev_tokens.size(); i++) { + if (ev_tokens[i] == "->") { + found_arrow = true; + if (i + 1 < ev_tokens.size()) ret_type = ev_tokens[i + 1]; + break; + } + } + if (found_arrow && ret_type != "void") { + node.error = "Event return type must be void (got: " + ret_type + ")"; + } + } + } + + run_type_inference(); +} + +void Editor1Pane::run_type_inference() { + GraphInference inference(type_pool_); + inference.run(graph_); +} + +void Editor1Pane::center_on_node(const FlowNode& node, ImVec2 canvas_size) { + canvas_offset_.x = -node.position.x - node.size.x * 0.5f + canvas_size.x * 0.5f / canvas_zoom_; + canvas_offset_.y = -node.position.y - node.size.y * 0.5f + canvas_size.y * 0.5f / canvas_zoom_; + highlight_node_id_ = node.id; + highlight_timer_ = 3.0f; +} + +void Editor1Pane::copy_selection() { + clipboard_nodes_.clear(); + clipboard_links_.clear(); + if (selected_nodes_.empty()) return; + + ImVec2 centroid = {0, 0}; + int count = 0; + for (auto& node : graph_.nodes) { + if (!selected_nodes_.count(node.id)) continue; + centroid.x += node.position.x; + centroid.y += node.position.y; + count++; + } + if (count > 0) { centroid.x /= count; centroid.y /= count; } + + std::map id_to_idx; + for (auto& node : graph_.nodes) { + if (!selected_nodes_.count(node.id)) continue; + int idx = (int)clipboard_nodes_.size(); + id_to_idx[node.id] = idx; + clipboard_nodes_.push_back({node.type_id, node.args, + {node.position.x - centroid.x, node.position.y - centroid.y}}); + } + + std::map> pin_owner; + for (auto& node : graph_.nodes) { + if (!selected_nodes_.count(node.id)) continue; + auto register_pin = [&](const FlowPin& p) { pin_owner[p.id] = {node.id, p.name}; }; + for (auto& p : node.triggers) register_pin(*p); + for (auto& p : node.inputs) register_pin(*p); + for (auto& p : node.outputs) register_pin(*p); + for (auto& p : node.nexts) register_pin(*p); + register_pin(node.lambda_grab); + register_pin(node.bang_pin); + } + for (auto& link : graph_.links) { + auto fi = pin_owner.find(link.from_pin); + auto ti = pin_owner.find(link.to_pin); + if (fi != pin_owner.end() && ti != pin_owner.end()) { + auto from_idx = id_to_idx[fi->second.first]; + auto to_idx = id_to_idx[ti->second.first]; + clipboard_links_.push_back({from_idx, to_idx, fi->second.second, ti->second.second}); + } + } +} + +void Editor1Pane::paste_at(ImVec2 canvas_pos) { + if (clipboard_nodes_.empty()) return; + + selected_nodes_.clear(); + std::vector new_guids; + + for (auto& cn : clipboard_nodes_) { + std::string guid = generate_guid(); + new_guids.push_back(guid); + ImVec2 pos = {canvas_pos.x + cn.offset.x, canvas_pos.y + cn.offset.y}; + int id = graph_.add_node(guid, to_vec2(pos), 0, 0); + + for (auto& node : graph_.nodes) { + if (node.id != id) continue; + node.type_id = cn.type_id; + node.args = cn.args; + node.parse_args(); + + auto* nt = find_node_type(cn.type_id); + if (nt) { + node.triggers.clear(); + node.inputs.clear(); + node.outputs.clear(); + node.nexts.clear(); + + for (int i = 0; i < nt->num_triggers; i++) { + std::string biname = (nt->trigger_ports && i < nt->num_triggers) ? nt->trigger_ports[i].name : ("bang_in" + std::to_string(i)); + node.triggers.push_back(make_pin("", biname, "", nullptr, FlowPin::BangTrigger)); + } + + bool is_expr_paste = is_any_of(cn.type_id, NodeTypeID::Expr, NodeTypeID::ExprBang); + int num_outputs = nt->outputs; + if (is_expr_paste) { + auto parsed = scan_slots(cn.args); + int total_top = parsed.total_pin_count(nt->inputs); + for (int i = 0; i < total_top; i++) { + bool il = parsed.is_lambda_slot(i); + std::string pn = il ? ("@"+std::to_string(i)) : std::to_string(i); + node.inputs.push_back(make_pin("", pn, "", nullptr, il ? FlowPin::Lambda : FlowPin::Input)); + } + if (!cn.args.empty()) { + auto tokens = tokenize_args(cn.args, false); + num_outputs = std::max(1, (int)tokens.size()); + } + } else { + auto info = compute_inline_args(cn.args, nt->inputs); + if (!info.error.empty()) node.error = info.error; + int ref_pins = (info.pin_slots.max_slot >= 0) ? (info.pin_slots.max_slot + 1) : 0; + for (int i = 0; i < ref_pins; i++) { + bool il = info.pin_slots.is_lambda_slot(i); + std::string pn = il ? ("@"+std::to_string(i)) : std::to_string(i); + node.inputs.push_back(make_pin("", pn, "", nullptr, il ? FlowPin::Lambda : FlowPin::Input)); + } + for (int i = info.num_inline_args; i < nt->inputs; i++) { + std::string pn; bool il = false; + if (nt->input_ports && i < nt->inputs) { + pn = nt->input_ports[i].name; + il = (nt->input_ports[i].kind == PortKind::Lambda); + } else pn = std::to_string(i); + node.inputs.push_back(make_pin("", pn, "", nullptr, il ? FlowPin::Lambda : FlowPin::Input)); + } + } + for (int i = 0; i < num_outputs; i++) { + std::string oname = (nt->output_ports && i < nt->outputs) ? nt->output_ports[i].name : ("out" + std::to_string(i)); + node.outputs.push_back(make_pin("", oname, "", nullptr, FlowPin::Output)); + } + for (int i = 0; i < nt->num_nexts; i++) { + std::string bname = (nt->next_ports && i < nt->num_nexts) ? nt->next_ports[i].name : ("bang" + std::to_string(i)); + node.nexts.push_back(make_pin("", bname, "", nullptr, FlowPin::BangNext)); + } + } + node.rebuild_pin_ids(); + selected_nodes_.insert(id); + break; + } + } + + for (auto& cl : clipboard_links_) { + if (cl.from_idx < 0 || cl.from_idx >= (int)new_guids.size()) continue; + if (cl.to_idx < 0 || cl.to_idx >= (int)new_guids.size()) continue; + std::string from_id = new_guids[cl.from_idx] + "." + cl.from_pin_name; + std::string to_id = new_guids[cl.to_idx] + "." + cl.to_pin_name; + graph_.add_link(from_id, to_id); + } + + resolve_type_based_pins(graph_); + mark_dirty(); +} + +// ============================================================ +// Editor1Pane::draw() — the big legacy canvas drawing function +// ============================================================ + +void Editor1Pane::draw() { + // Tick highlight timer + if (highlight_timer_ > 0.0f) { + highlight_timer_ -= ImGui::GetIO().DeltaTime; + if (highlight_timer_ <= 0.0f) { + highlight_timer_ = 0.0f; + highlight_node_id_ = -1; + } + } + + // Validate only when graph structure changes + if (graph_.dirty) { + validate_nodes(); + graph_.dirty = false; + } + + // Check debounced save + check_debounced_save(); + + ImVec2 canvas_origin = ImGui::GetCursorScreenPos(); + ImVec2 canvas_size = ImGui::GetContentRegionAvail(); + ImDrawList* dl = ImGui::GetWindowDrawList(); + + // Background + dl->AddRectFilled(canvas_origin, + {canvas_origin.x + canvas_size.x, canvas_origin.y + canvas_size.y}, COL_BG); + + // Safety: remove any empty-named nodes that aren't currently being edited + std::erase_if(graph_.nodes, [&](auto& n) { + if (n.id == editing_node_) return false; + if (n.guid.empty()) return true; + return false; + }); + + // Grid + float grid = GRID_SIZE * canvas_zoom_; + if (grid > 4.0f) { + float ox = std::fmod(canvas_offset_.x * canvas_zoom_, grid); + float oy = std::fmod(canvas_offset_.y * canvas_zoom_, grid); + for (float x = ox; x < canvas_size.x; x += grid) + dl->AddLine({canvas_origin.x + x, canvas_origin.y}, + {canvas_origin.x + x, canvas_origin.y + canvas_size.y}, COL_GRID); + for (float y = oy; y < canvas_size.y; y += grid) + dl->AddLine({canvas_origin.x, canvas_origin.y + y}, + {canvas_origin.x + canvas_size.x, canvas_origin.y + y}, COL_GRID); + } + + ImGui::InvisibleButton("##canvas", canvas_size, + ImGuiButtonFlags_MouseButtonLeft | + ImGuiButtonFlags_MouseButtonMiddle | + ImGuiButtonFlags_MouseButtonRight); + bool canvas_hovered = ImGui::IsItemHovered(); + ImVec2 mouse_pos = ImGui::GetMousePos(); + + // --- Canvas pan --- + if (canvas_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Middle)) { + canvas_dragging_ = true; + canvas_drag_start_ = mouse_pos; + } + if (canvas_dragging_) { + if (ImGui::IsMouseDown(ImGuiMouseButton_Middle)) { + ImVec2 delta = {mouse_pos.x - canvas_drag_start_.x, mouse_pos.y - canvas_drag_start_.y}; + canvas_offset_.x += delta.x / canvas_zoom_; + canvas_offset_.y += delta.y / canvas_zoom_; + canvas_drag_start_ = mouse_pos; + schedule_save(); + } else { canvas_dragging_ = false; } + } + + // --- Canvas zoom --- + if (canvas_hovered) { + float wheel = ImGui::GetIO().MouseWheel; + if (std::abs(wheel) > 0.01f) { + float zf = std::pow(1.1f, wheel); + ImVec2 mc = screen_to_canvas(mouse_pos, canvas_origin); + canvas_zoom_ *= zf; + canvas_zoom_ = std::clamp(canvas_zoom_, 0.2f, 5.0f); + ImVec2 mc2 = screen_to_canvas(mouse_pos, canvas_origin); + canvas_offset_.x += mc2.x - mc.x; + canvas_offset_.y += mc2.y - mc.y; + schedule_save(); + } + } + + // Helper: hit test node at canvas pos + auto hit_test_node = [&](ImVec2 mc) -> int { + for (int i = (int)graph_.nodes.size() - 1; i >= 0; i--) { + auto& node = graph_.nodes[i]; + if (node.imported || node.shadow) continue; + if (mc.x >= node.position.x && mc.x <= node.position.x + node.size.x && + mc.y >= node.position.y && mc.y <= node.position.y + node.size.y) + return node.id; + } + return -1; + }; + + // --- Double-click on node: edit --- + if (canvas_hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + ImVec2 mc = screen_to_canvas(mouse_pos, canvas_origin); + int hit_id = hit_test_node(mc); + if (hit_id >= 0) { + for (auto& node : graph_.nodes) { + if (node.id == hit_id) { + editing_node_ = node.id; + creating_new_node_ = false; + dragging_node_ = -1; + edit_buf_ = node.edit_text(); + edit_just_opened_ = true; + break; + } + } + } + } + // --- Single click --- + else if (canvas_hovered && editing_link_ < 0 && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + if (editing_node_ >= 0) { + if (creating_new_node_ && editing_node_ > 0) graph_.remove_node(editing_node_); + editing_node_ = -1; + creating_new_node_ = false; + selected_nodes_.clear(); + } else { + auto pin_hit = hit_test_pin(mouse_pos, canvas_origin); + if (!pin_hit.pin_id.empty()) { + dragging_link_from_pin_ = pin_hit.pin_id; + dragging_link_from_output_ = true; + dragging_node_ = -1; + dragging_selection_ = false; + } else { + dragging_link_from_pin_.clear(); + ImVec2 mc = screen_to_canvas(mouse_pos, canvas_origin); + int hit_id = hit_test_node(mc); + + if (hit_id >= 0) { + if (selected_nodes_.count(hit_id)) { + dragging_selection_ = true; + dragging_node_ = -1; + } else { + selected_nodes_.clear(); + selected_nodes_.insert(hit_id); + dragging_selection_ = true; + dragging_node_ = -1; + } + } else { + int wire_hit = hit_test_link(mouse_pos, canvas_origin); + if (wire_hit >= 0) { + dragging_node_ = -1; + dragging_selection_ = false; + } else { + box_selecting_ = true; + box_select_start_ = mouse_pos; + dragging_node_ = -1; + dragging_selection_ = false; + } + } + } + } + } + + // --- Box selection --- + if (box_selecting_) { + if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { + float dx = mouse_pos.x - box_select_start_.x; + float dy = mouse_pos.y - box_select_start_.y; + float dist = dx*dx + dy*dy; + if (dist > 25.0f) { + ImVec2 a = box_select_start_; + ImVec2 b = mouse_pos; + ImVec2 tl_box = {std::min(a.x, b.x), std::min(a.y, b.y)}; + ImVec2 br_box = {std::max(a.x, b.x), std::max(a.y, b.y)}; + dl->AddRectFilled(tl_box, br_box, IM_COL32(100, 150, 255, 40)); + dl->AddRect(tl_box, br_box, IM_COL32(100, 150, 255, 180)); + + ImVec2 ca = screen_to_canvas(tl_box, canvas_origin); + ImVec2 cb = screen_to_canvas(br_box, canvas_origin); + selected_nodes_.clear(); + for (auto& node : graph_.nodes) { + if (node.imported || node.shadow) continue; + if (node.position.x + node.size.x >= ca.x && node.position.x <= cb.x && + node.position.y + node.size.y >= ca.y && node.position.y <= cb.y) + selected_nodes_.insert(node.id); + } + } + } else { + float dx = mouse_pos.x - box_select_start_.x; + float dy = mouse_pos.y - box_select_start_.y; + if (dx*dx + dy*dy <= 25.0f) { + if (!selected_nodes_.empty()) { + selected_nodes_.clear(); + } else { + creating_new_node_ = true; + editing_node_ = 0; + new_node_pos_ = screen_to_canvas(mouse_pos, canvas_origin); + edit_buf_.clear(); + edit_just_opened_ = true; + } + } + box_selecting_ = false; + } + } + + // Link dragging + if (!dragging_link_from_pin_.empty()) { + if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) { + auto pin_hit = hit_test_pin(mouse_pos, canvas_origin); + if (!pin_hit.pin_id.empty() && pin_hit.pin_id != dragging_link_from_pin_) { + auto from_dir = FlowPin::Input; + for (auto& node : graph_.nodes) { + for (auto& p : node.triggers) if (p->id == dragging_link_from_pin_) from_dir = p->direction; + for (auto& p : node.inputs) if (p->id == dragging_link_from_pin_) from_dir = p->direction; + for (auto& p : node.outputs) if (p->id == dragging_link_from_pin_) from_dir = p->direction; + for (auto& p : node.nexts) if (p->id == dragging_link_from_pin_) from_dir = p->direction; + if (node.lambda_grab.id == dragging_link_from_pin_) from_dir = node.lambda_grab.direction; + if (node.bang_pin.id == dragging_link_from_pin_) from_dir = node.bang_pin.direction; + } + + auto is_source = [](FlowPin::Direction d) { + return d == FlowPin::Output || d == FlowPin::BangNext || + d == FlowPin::LambdaGrab || d == FlowPin::BangTrigger; + }; + auto is_dest = [](FlowPin::Direction d) { + return d == FlowPin::Input || d == FlowPin::BangTrigger || + d == FlowPin::Lambda; + }; + + std::string from_pin, to_pin; + bool valid = false; + if (is_source(from_dir) && is_dest(pin_hit.dir)) { + from_pin = dragging_link_from_pin_; + to_pin = pin_hit.pin_id; + valid = true; + } else if (is_source(pin_hit.dir) && is_dest(from_dir)) { + from_pin = pin_hit.pin_id; + to_pin = dragging_link_from_pin_; + valid = true; + } + + if (valid) { + FlowPin::Direction to_dir = FlowPin::Input; + for (auto& node : graph_.nodes) { + for (auto& p : node.triggers) if (p->id == to_pin) to_dir = FlowPin::BangTrigger; + for (auto& p : node.inputs) if (p->id == to_pin) to_dir = p->direction; + } + bool allow_multi = (to_dir == FlowPin::BangTrigger || to_dir == FlowPin::Lambda); + if (!allow_multi) + std::erase_if(graph_.links, [&](auto& l) { return l.to_pin == to_pin; }); + graph_.add_link(from_pin, to_pin); + mark_dirty(); + } + } + dragging_link_from_pin_.clear(); + } + } + + // Grab pending: waiting for drag threshold before detaching links (right mouse) + if (grab_pending_ && !grabbed_pin_.empty()) { + if (ImGui::IsMouseDown(ImGuiMouseButton_Right)) { + float dx = mouse_pos.x - grab_start_.x; + float dy = mouse_pos.y - grab_start_.y; + if (dx*dx + dy*dy > 25.0f) { + grab_pending_ = false; + for (auto& l : graph_.links) { + if (grab_is_output_) { + if (l.from_pin == grabbed_pin_) + grabbed_links_.push_back({l.from_pin, l.to_pin}); + } else { + if (l.to_pin == grabbed_pin_) + grabbed_links_.push_back({l.from_pin, l.to_pin}); + } + } + if (!grabbed_links_.empty()) { + if (grab_is_output_) + std::erase_if(graph_.links, [&](auto& l) { return l.from_pin == grabbed_pin_; }); + else + std::erase_if(graph_.links, [&](auto& l) { return l.to_pin == grabbed_pin_; }); + graph_.dirty = true; + } else { + grabbed_pin_.clear(); + } + } + } else { + grab_pending_ = false; + grabbed_pin_.clear(); + } + } + + // Grabbed links: actively dragging detached connections (right mouse) + if (!grabbed_links_.empty() && !grab_pending_) { + if (ImGui::IsMouseDown(ImGuiMouseButton_Right)) { + for (auto& gl : grabbed_links_) { + ImVec2 anchor = {}; + bool found = false; + std::string anchor_id = grab_is_output_ ? gl.to_pin : gl.from_pin; + for (auto& n : graph_.nodes) { + for (auto& p : n.outputs) if (p->id == anchor_id) { anchor = get_pin_pos(n, *p, canvas_origin); found = true; } + for (auto& p : n.nexts) if (p->id == anchor_id) { anchor = get_pin_pos(n, *p, canvas_origin); found = true; } + if (n.lambda_grab.id == anchor_id) { anchor = get_pin_pos(n, n.lambda_grab, canvas_origin); found = true; } + if (n.bang_pin.id == anchor_id) { anchor = get_pin_pos(n, n.bang_pin, canvas_origin); found = true; } + for (auto& p : n.triggers) if (p->id == anchor_id) { anchor = get_pin_pos(n, *p, canvas_origin); found = true; } + for (auto& p : n.inputs) if (p->id == anchor_id) { anchor = get_pin_pos(n, *p, canvas_origin); found = true; } + } + if (found) { + ImU32 col = COL_LINK_DRAG; + if (grab_is_output_) + draw_vbezier(dl, mouse_pos, anchor, col, 2.5f, canvas_zoom_); + else + draw_vbezier(dl, anchor, mouse_pos, col, 2.5f, canvas_zoom_); + } + } + } else { + auto pin_hit = hit_test_pin(mouse_pos, canvas_origin); + bool reconnected = false; + if (!pin_hit.pin_id.empty()) { + if (grab_is_output_) { + if (pin_hit.dir == FlowPin::Output || pin_hit.dir == FlowPin::BangNext || pin_hit.dir == FlowPin::LambdaGrab) { + for (auto& gl : grabbed_links_) + graph_.add_link(pin_hit.pin_id, gl.to_pin); + reconnected = true; + mark_dirty(); + } + } else { + if (pin_hit.dir == FlowPin::Input || pin_hit.dir == FlowPin::BangTrigger || pin_hit.dir == FlowPin::Lambda) { + if (pin_hit.dir != FlowPin::BangTrigger && pin_hit.dir != FlowPin::Lambda) + std::erase_if(graph_.links, [&](auto& l) { return l.to_pin == pin_hit.pin_id; }); + for (auto& gl : grabbed_links_) + graph_.add_link(gl.from_pin, pin_hit.pin_id); + reconnected = true; + mark_dirty(); + } + } + } + if (!reconnected) { + for (auto& gl : grabbed_links_) + graph_.add_link(gl.from_pin, gl.to_pin); + } + grabbed_links_.clear(); + grabbed_pin_.clear(); + } + } + + // Selection dragging (move all selected nodes) + if (dragging_selection_ && ImGui::IsMouseDown(ImGuiMouseButton_Left)) { + ImVec2 delta = ImGui::GetIO().MouseDelta; + for (auto& node : graph_.nodes) { + if (selected_nodes_.count(node.id)) { + node.position.x += delta.x / canvas_zoom_; + node.position.y += delta.y / canvas_zoom_; + } + } + } + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + if (dragging_selection_) mark_dirty(); + dragging_selection_ = false; + dragging_node_ = -1; + } + + // --- Keyboard shortcuts --- + if (canvas_hovered && editing_node_ < 0) { + bool ctrl = ImGui::GetIO().KeyCtrl; + if (ctrl && ImGui::IsKeyPressed(ImGuiKey_C)) { + copy_selection(); + } + if (ctrl && ImGui::IsKeyPressed(ImGuiKey_V)) { + ImVec2 mc = screen_to_canvas(mouse_pos, canvas_origin); + paste_at(mc); + } + if (ctrl && ImGui::IsKeyPressed(ImGuiKey_D)) { + auto saved_nodes = clipboard_nodes_; + auto saved_links = clipboard_links_; + copy_selection(); + ImVec2 mc = screen_to_canvas(mouse_pos, canvas_origin); + paste_at(mc); + clipboard_nodes_ = saved_nodes; + clipboard_links_ = saved_links; + } + if (ImGui::IsKeyPressed(ImGuiKey_Delete) && !selected_nodes_.empty()) { + for (int id : selected_nodes_) + graph_.remove_node(id); + selected_nodes_.clear(); + mark_dirty(); + } + if (ctrl && ImGui::IsKeyPressed(ImGuiKey_Z)) { + if (ImGui::GetIO().KeyShift) + redo(); + else + undo(); + selected_nodes_.clear(); + } + if (ctrl && ImGui::IsKeyPressed(ImGuiKey_Y)) { + redo(); + selected_nodes_.clear(); + } + } + + // --- Right click: track start position and check for pin grab --- + static ImVec2 right_click_start = {}; + if (canvas_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { + right_click_start = mouse_pos; + auto pin_hit = hit_test_pin(mouse_pos, canvas_origin); + if (!pin_hit.pin_id.empty()) { + grabbed_links_.clear(); + grabbed_pin_ = pin_hit.pin_id; + grab_is_output_ = (pin_hit.dir == FlowPin::Output || pin_hit.dir == FlowPin::BangNext || + pin_hit.dir == FlowPin::LambdaGrab); + grab_pending_ = true; + grab_start_ = mouse_pos; + } + } + + // --- Right click release: disconnect pin, delete link, or delete node --- + if (canvas_hovered && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) { + float rdx = mouse_pos.x - right_click_start.x; + float rdy = mouse_pos.y - right_click_start.y; + bool was_drag = (rdx*rdx + rdy*rdy > 25.0f); + + if (!was_drag) { + auto pin_hit = hit_test_pin(mouse_pos, canvas_origin); + if (!pin_hit.pin_id.empty()) { + std::erase_if(graph_.links, [&](auto& l) { + return l.from_pin == pin_hit.pin_id || l.to_pin == pin_hit.pin_id; + }); + graph_.dirty = true; + } + else { + int lid = hit_test_link(mouse_pos, canvas_origin); + if (lid >= 0) { + graph_.remove_link(lid); + } else { + ImVec2 mc = screen_to_canvas(mouse_pos, canvas_origin); + for (int i = (int)graph_.nodes.size() - 1; i >= 0; i--) { + auto& node = graph_.nodes[i]; + if (mc.x >= node.position.x && mc.x <= node.position.x + node.size.x && + mc.y >= node.position.y && mc.y <= node.position.y + node.size.y) { + graph_.remove_node(node.id); + if (editing_node_ == node.id) { + editing_node_ = -1; + creating_new_node_ = false; + } + break; + } + } + } + } + mark_dirty(); + } // !was_drag + } + + // --- Build shadow filter sets for drawing --- + std::set shadow_guids; + shadow_connected_pins_.clear(); + for (auto& node : graph_.nodes) + if (node.shadow) shadow_guids.insert(node.guid); + for (auto& link : graph_.links) { + auto d1 = link.from_pin.find('.'); + if (d1 != std::string::npos && shadow_guids.count(link.from_pin.substr(0, d1))) + shadow_connected_pins_.insert(link.to_pin); + auto d2 = link.to_pin.find('.'); + if (d2 != std::string::npos && shadow_guids.count(link.to_pin.substr(0, d2))) + shadow_connected_pins_.insert(link.from_pin); + } + + // --- Draw links (skip links involving shadow nodes) --- + for (auto& link : graph_.links) { + auto d1 = link.from_pin.find('.'); + auto d2 = link.to_pin.find('.'); + if (d1 != std::string::npos && shadow_guids.count(link.from_pin.substr(0, d1))) continue; + if (d2 != std::string::npos && shadow_guids.count(link.to_pin.substr(0, d2))) continue; + draw_link(dl, link, canvas_origin); + } + + // --- Draw link being dragged --- + if (!dragging_link_from_pin_.empty() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) { + for (auto& node : graph_.nodes) { + ImVec2 from = {}; + bool from_grab = false; + bool from_bang_pin = false; + bool found = false; + for (auto& pin : node.outputs) { + if (pin->id == dragging_link_from_pin_) { from = get_pin_pos(node, *pin, canvas_origin); found = true; break; } + } + if (!found) for (auto& pin : node.nexts) { + if (pin->id == dragging_link_from_pin_) { from = get_pin_pos(node, *pin, canvas_origin); found = true; break; } + } + if (!found) for (auto& pin : node.inputs) { + if (pin->id == dragging_link_from_pin_) { from = get_pin_pos(node, *pin, canvas_origin); found = true; break; } + } + if (!found) for (auto& pin : node.triggers) { + if (pin->id == dragging_link_from_pin_) { from = get_pin_pos(node, *pin, canvas_origin); found = true; break; } + } + if (!found && node.lambda_grab.id == dragging_link_from_pin_) { + from = get_pin_pos(node, node.lambda_grab, canvas_origin); + found = true; + from_grab = true; + } + if (!found && node.bang_pin.id == dragging_link_from_pin_) { + from = get_pin_pos(node, node.bang_pin, canvas_origin); + found = true; + from_bang_pin = true; + } + if (found) { + auto target = hit_test_pin(mouse_pos, canvas_origin); + bool valid_target = !target.pin_id.empty() && target.pin_id != dragging_link_from_pin_; + ImU32 col = valid_target ? COL_PIN_HOVER : COL_LINK_DRAG; + if (from_grab) { + float dx = std::max(std::abs(mouse_pos.x - from.x) * 0.5f, 30.0f * canvas_zoom_); + float dy = std::max(std::abs(mouse_pos.y - from.y) * 0.5f, 30.0f * canvas_zoom_); + dl->AddBezierCubic(from, {from.x - dx, from.y}, {mouse_pos.x, mouse_pos.y - dy}, + mouse_pos, col, 2.5f * canvas_zoom_); + } else if (from_bang_pin) { + float dx = std::max(std::abs(mouse_pos.x - from.x) * 0.5f, 30.0f * canvas_zoom_); + dl->AddBezierCubic(from, {from.x + dx, from.y}, {mouse_pos.x - dx, mouse_pos.y}, + mouse_pos, col, 2.5f * canvas_zoom_); + } else { + draw_vbezier(dl, from, mouse_pos, col, 2.5f, canvas_zoom_); + } + goto done_drag; + } + } + done_drag:; + } + + // --- Draw nodes --- + auto hovered_pin = hit_test_pin(mouse_pos, canvas_origin); + for (auto& node : graph_.nodes) { + if (node.imported || node.shadow) continue; + draw_node(dl, node, canvas_origin); + } + + // Pin hover highlight + if (!hovered_pin.pin_id.empty()) { + for (auto& node : graph_.nodes) { + PinShape io_shape = PinShape::Signal; + float pr = PIN_RADIUS * canvas_zoom_; + auto check = [&](auto& pins, PinShape shape) { + for (auto& pin : pins) + if (pin->id == hovered_pin.pin_id) { + ImVec2 pp = get_pin_pos(node, *pin, canvas_origin); + draw_pin_highlight(dl, pp, pr, COL_PIN_HOVER, shape, canvas_zoom_); + } + }; + check(node.triggers, PinShape::Square); + for (auto& pin : node.inputs) + if (pin->id == hovered_pin.pin_id) { + ImVec2 pp = get_pin_pos(node, *pin, canvas_origin); + PinShape shape = (pin->direction == FlowPin::Lambda) ? PinShape::LambdaDown : io_shape; + draw_pin_highlight(dl, pp, pr, COL_PIN_HOVER, shape, canvas_zoom_); + } + check(node.nexts, PinShape::Square); + check(node.outputs, io_shape); + if (node.lambda_grab.id == hovered_pin.pin_id) { + ImVec2 pp = get_pin_pos(node, node.lambda_grab, canvas_origin); + draw_pin_highlight(dl, pp, pr, COL_PIN_HOVER, PinShape::LambdaLeft, canvas_zoom_); + } + } + } + + // --- Tooltips --- + if (canvas_hovered && editing_node_ < 0 && editing_link_ < 0) { + if (!hovered_pin.pin_id.empty()) { + for (auto& node : graph_.nodes) { + if (node.id != hovered_pin.node_id) continue; + auto find_pin = [&](auto& pins) -> const FlowPin* { + for (auto& p : pins) if (p->id == hovered_pin.pin_id) return p.get(); + return nullptr; + }; + const FlowPin* pin = find_pin(node.triggers); + if (!pin) pin = find_pin(node.inputs); + if (!pin) pin = find_pin(node.outputs); + if (!pin) pin = find_pin(node.nexts); + if (!pin && node.lambda_grab.id == hovered_pin.pin_id) pin = &node.lambda_grab; + if (!pin && node.bang_pin.id == hovered_pin.pin_id) pin = &node.bang_pin; + if (pin) { + auto [port_name, port_desc] = get_port_desc(node, *pin); + std::string type_str; + if (pin->resolved_type) + type_str = type_to_string(pin->resolved_type); + else if (pin->direction == FlowPin::BangTrigger || pin->direction == FlowPin::BangNext) + type_str = "bang"; + else + type_str = "?"; + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(canvas_zoom_); + ImGui::TextUnformatted((port_name + " : " + type_str).c_str()); + if (!port_desc.empty()) + ImGui::TextDisabled("%s", port_desc.c_str()); + ImGui::EndTooltip(); + } + break; + } + } else { + int lid = hit_test_link(mouse_pos, canvas_origin); + if (lid >= 0) { + for (auto& link : graph_.links) { + if (link.id != lid) continue; + std::string from_label, to_label; + for (auto& n : graph_.nodes) { + for (auto& p : n.outputs) if (p->id == link.from_pin) from_label = pin_label(n, *p); + for (auto& p : n.nexts) if (p->id == link.from_pin) from_label = pin_label(n, *p); + for (auto& p : n.triggers) if (p->id == link.from_pin) from_label = pin_label(n, *p); + if (n.lambda_grab.id == link.from_pin) from_label = pin_label(n, n.lambda_grab); + if (n.bang_pin.id == link.from_pin) from_label = pin_label(n, n.bang_pin); + for (auto& p : n.inputs) if (p->id == link.to_pin) to_label = pin_label(n, *p); + for (auto& p : n.triggers) if (p->id == link.to_pin) to_label = pin_label(n, *p); + } + if (!from_label.empty() && !to_label.empty()) { + auto* fp = graph_.find_pin(link.from_pin); + auto* tp = graph_.find_pin(link.to_pin); + std::string from_type_str = (fp && fp->resolved_type) ? type_to_string(fp->resolved_type) : "?"; + std::string to_type_str = (tp && tp->resolved_type) ? type_to_string(tp->resolved_type) : "?"; + bool type_err = !link.error.empty(); + if (!type_err && fp && tp && fp->resolved_type && tp->resolved_type && + !fp->resolved_type->is_generic && !tp->resolved_type->is_generic) + type_err = !types_compatible(fp->resolved_type, tp->resolved_type); + + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(canvas_zoom_); + if (!link.net_name.empty()) { + ImGui::TextColored({0.7f, 0.9f, 1.0f, 1.0f}, "%s", link.net_name.c_str()); + } + ImGui::TextUnformatted((from_label + " -> " + to_label).c_str()); + ImGui::TextDisabled("%s -> %s", from_type_str.c_str(), to_type_str.c_str()); + if (!link.error.empty()) + ImGui::TextColored({1.0f, 0.2f, 0.2f, 1.0f}, "%s", link.error.c_str()); + else if (type_err) + ImGui::TextColored({1.0f, 0.2f, 0.2f, 1.0f}, "Type mismatch!"); + ImGui::TextDisabled("Click to rename wire"); + ImGui::EndTooltip(); + + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + editing_link_ = link.id; + link_edit_buf_ = link.net_name.empty() ? "$" : link.net_name; + link_edit_just_opened_ = true; + } + } + break; + } + } else { + ImVec2 mc = screen_to_canvas(mouse_pos, canvas_origin); + for (int i = (int)graph_.nodes.size() - 1; i >= 0; i--) { + auto& node = graph_.nodes[i]; + if (mc.x >= node.position.x && mc.x <= node.position.x + node.size.x && + mc.y >= node.position.y && mc.y <= node.position.y + node.size.y) { + auto* nt = find_node_type(node.type_id); + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(canvas_zoom_); + ImGui::TextUnformatted(node_display_name(node).c_str()); + if (nt && nt->desc) + ImGui::TextDisabled("%s", nt->desc); + if (!node.error.empty()) { + ImGui::Separator(); + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 100, 100, 255)); + ImGui::TextUnformatted("Errors:"); + ImGui::TextUnformatted(node.error.c_str()); + ImGui::PopStyleColor(); + } + ImGui::TextDisabled("(%s)", node.guid.c_str()); + ImGui::EndTooltip(); + break; + } + } + } + } + } + + // --- Name editing: inline inside the node --- + if (editing_node_ >= 0) { + FlowNode* edit_node = nullptr; + for (auto& node : graph_.nodes) { + if (node.id == editing_node_) { edit_node = &node; break; } + } + ImVec2 edit_pos = edit_node ? to_imvec(edit_node->position) : new_node_pos_; + ImVec2 edit_size = edit_node ? to_imvec(edit_node->size) : ImVec2{NODE_MIN_WIDTH, NODE_HEIGHT}; + + { + ImVec2 tl = canvas_to_screen(edit_pos, canvas_origin); + ImVec2 br = canvas_to_screen({edit_pos.x + edit_size.x, + edit_pos.y + edit_size.y}, canvas_origin); + float nw = br.x - tl.x; + + float text_w = ImGui::CalcTextSize(edit_buf_.c_str()).x * canvas_zoom_ + 40.0f * canvas_zoom_; + float scaled_min_w = std::max({nw, 160.0f * canvas_zoom_, text_w}); + ImGui::SetNextWindowPos(tl); + ImGui::SetNextWindowSize({scaled_min_w, 0}); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {2 * canvas_zoom_, 2 * canvas_zoom_}); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {4 * canvas_zoom_, 2 * canvas_zoom_}); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {4 * canvas_zoom_, 2 * canvas_zoom_}); + ImGui::Begin("##name_edit", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar); + ImGui::SetWindowFontScale(canvas_zoom_); + + if (edit_just_opened_) { + ImGui::SetKeyboardFocusHere(); + edit_just_opened_ = false; + } + + char buf[128]; + strncpy(buf, edit_buf_.c_str(), sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + + bool* cursor_to_end_ptr = &edit_cursor_to_end_; + auto edit_callback = [](ImGuiInputTextCallbackData* data) -> int { + bool* flag = (bool*)data->UserData; + if (*flag) { + data->CursorPos = data->BufTextLen; + data->SelectionStart = data->SelectionEnd = data->CursorPos; + *flag = false; + } + return 0; + }; + + ImGui::SetNextItemWidth(-1); + bool committed = ImGui::InputText("##edit", buf, sizeof(buf), + ImGuiInputTextFlags_EnterReturnsTrue | + ImGuiInputTextFlags_CallbackAlways, + edit_callback, cursor_to_end_ptr); + edit_buf_ = buf; + + std::string first_word = edit_buf_; + std::string rest_args; + auto space_pos = edit_buf_.find(' '); + if (space_pos != std::string::npos) { + first_word = edit_buf_.substr(0, space_pos); + rest_args = edit_buf_.substr(space_pos + 1); + } + + if (space_pos == std::string::npos) { + for (int i = 0; i < NUM_NODE_TYPES; i++) { + std::string nt_name(NODE_TYPES[i].name); + if (first_word.empty() || (nt_name.find(first_word) != std::string::npos && nt_name != first_word)) { + if (ImGui::Selectable(NODE_TYPES[i].name)) { + edit_buf_ = nt_name + " "; + edit_just_opened_ = true; + edit_cursor_to_end_ = true; + } + } + } + } + + if (committed) do { + std::string first_word, rest_args; + auto sp = edit_buf_.find(' '); + if (sp != std::string::npos) { + first_word = edit_buf_.substr(0, sp); + rest_args = edit_buf_.substr(sp + 1); + } else { + first_word = edit_buf_; + } + + std::string node_type = first_word; + if (node_type.empty()) break; + + if (creating_new_node_ && !edit_node) { + int id = graph_.add_node("", to_vec2(new_node_pos_), 0, 0); + for (auto& n : graph_.nodes) { + if (n.id == id) { edit_node = &n; break; } + } + editing_node_ = id; + } + if (!edit_node) break; + + auto* nt = find_node_type(node_type.c_str()); + if (!nt) { + nt = find_node_type("expr"); + node_type = "expr"; + rest_args = edit_buf_; + } + int default_triggers = nt ? nt->num_triggers : 0; + int default_inputs = nt ? nt->inputs : 0; + int default_outputs = nt ? nt->outputs : 0; + int default_nexts = nt ? nt->num_nexts : 0; + + auto& node = *edit_node; + if (node.guid.empty()) + node.guid = generate_guid(); + node.type_id = node_type_id_from_string(node_type.c_str()); + node.args = rest_args; + node.parse_args(); + graph_.dirty = true; + creating_new_node_ = false; + + auto resize_pins = [&](PinVec& pins, int needed, + const std::vector& names, + FlowPin::Direction dir, bool is_output) { + for (int i = 0; i < std::min((int)pins.size(), needed); i++) + pins[i]->name = names[i]; + for (int i = (int)pins.size(); i < needed; i++) + pins.push_back(make_pin("", names[i], "", nullptr, dir)); + while ((int)pins.size() > needed) { + auto pid = pins.back()->id; + if (is_output) + std::erase_if(graph_.links, [&pid](auto& l) { return l.from_pin == pid; }); + else + std::erase_if(graph_.links, [&pid](auto& l) { return l.to_pin == pid; }); + pins.pop_back(); + } + }; + + auto make_names = [](const std::string& prefix, int count) { + std::vector names; + for (int i = 0; i < count; i++) names.push_back(prefix + std::to_string(i)); + return names; + }; + + int needed_outputs = default_outputs; + bool is_expr_type = is_any_of(node.type_id, NodeTypeID::Expr, NodeTypeID::ExprBang); + + struct DesiredPin { std::string name; FlowPin::Direction dir; }; + std::vector desired_inputs; + + if (node.type_id == NodeTypeID::New) { + auto tokens = tokenize_args(rest_args, false); + std::string inst_type_name = tokens.empty() ? "" : tokens[0]; + auto* type_node = find_type_node(graph_, inst_type_name); + if (type_node) { + auto fields = parse_type_fields(*type_node); + for (auto& field : fields) + desired_inputs.push_back({field.name, FlowPin::Input}); + } + needed_outputs = 1; + } else if (node.type_id == NodeTypeID::EventBang) { + auto tokens = tokenize_args(rest_args, false); + std::string event_name = tokens.empty() ? "" : tokens[0]; + auto* event_decl = find_event_node(graph_, event_name); + if (event_decl) { + auto args = parse_event_args(*event_decl, graph_); + std::vector out_names; + for (auto& a : args) out_names.push_back(a.name); + needed_outputs = (int)out_names.size(); + for (int i = 0; i < std::min((int)node.outputs.size(), needed_outputs); i++) + node.outputs[i]->name = out_names[i]; + for (int i = (int)node.outputs.size(); i < needed_outputs; i++) + node.outputs.push_back(make_pin("", out_names[i], "", nullptr, FlowPin::Output)); + while ((int)node.outputs.size() > needed_outputs) { + auto pid = node.outputs.back()->id; + std::erase_if(graph_.links, [&pid](auto& l) { return l.from_pin == pid; }); + node.outputs.pop_back(); + } + needed_outputs = -1; + } + } else { + if (is_expr_type) { + auto parsed = scan_slots(rest_args); + int total_top = parsed.total_pin_count(default_inputs); + for (int i = 0; i < total_top; i++) { + bool is_lambda = parsed.is_lambda_slot(i); + std::string pin_name = is_lambda ? ("@" + std::to_string(i)) : std::to_string(i); + desired_inputs.push_back({pin_name, is_lambda ? FlowPin::Lambda : FlowPin::Input}); + } + if (!node.args.empty()) { + auto tokens = tokenize_args(rest_args, false); + needed_outputs = std::max(1, (int)tokens.size()); + } + } else if (node_type == "cast" || node_type == "new") { + for (int i = 0; i < default_inputs; i++) { + std::string pin_name; + bool is_lambda = false; + if (nt && nt->input_ports && i < nt->inputs) { + pin_name = nt->input_ports[i].name; + is_lambda = (nt->input_ports[i].kind == PortKind::Lambda); + } else { + pin_name = std::to_string(i); + } + desired_inputs.push_back({pin_name, is_lambda ? FlowPin::Lambda : FlowPin::Input}); + } + } else { + auto info = compute_inline_args(rest_args, default_inputs); + if (!info.error.empty()) node.error = info.error; + int ref_pins = (info.pin_slots.max_slot >= 0) ? (info.pin_slots.max_slot + 1) : 0; + for (int i = 0; i < ref_pins; i++) { + bool is_lambda = info.pin_slots.is_lambda_slot(i); + std::string pin_name = is_lambda ? ("@" + std::to_string(i)) : std::to_string(i); + desired_inputs.push_back({pin_name, is_lambda ? FlowPin::Lambda : FlowPin::Input}); + } + for (int i = info.num_inline_args; i < default_inputs; i++) { + std::string pin_name; + bool is_lambda = false; + if (nt && nt->input_ports && i < nt->inputs) { + pin_name = nt->input_ports[i].name; + is_lambda = (nt->input_ports[i].kind == PortKind::Lambda); + } else { + pin_name = std::to_string(i); + } + desired_inputs.push_back({pin_name, is_lambda ? FlowPin::Lambda : FlowPin::Input}); + } + } + } + + // Resize inputs (unified data + lambda), preserving connections + { + int needed = (int)desired_inputs.size(); + for (int i = 0; i < std::min((int)node.inputs.size(), needed); i++) { + node.inputs[i]->name = desired_inputs[i].name; + node.inputs[i]->direction = desired_inputs[i].dir; + } + for (int i = (int)node.inputs.size(); i < needed; i++) + node.inputs.push_back(make_pin("", desired_inputs[i].name, "", nullptr, desired_inputs[i].dir)); + while ((int)node.inputs.size() > needed) { + auto pid = node.inputs.back()->id; + std::erase_if(graph_.links, [&pid](auto& l) { return l.to_pin == pid; }); + node.inputs.pop_back(); + } + } + + resize_pins(node.triggers, default_triggers, + make_names("bang_in", default_triggers), FlowPin::BangTrigger, false); + if (needed_outputs >= 0) + resize_pins(node.outputs, needed_outputs, + make_names("out", needed_outputs), FlowPin::Output, true); + resize_pins(node.nexts, default_nexts, + make_names("bang", default_nexts), FlowPin::BangNext, true); + + auto update_pin_ids = [&](PinVec& pins) { + for (auto& p : pins) { + std::string new_id = node.pin_id(p->name); + if (p->id != new_id) { + for (auto& l : graph_.links) { + if (l.from_pin == p->id) l.from_pin = new_id; + if (l.to_pin == p->id) l.to_pin = new_id; + } + p->id = new_id; + } + } + }; + update_pin_ids(node.triggers); + update_pin_ids(node.inputs); + update_pin_ids(node.outputs); + update_pin_ids(node.nexts); + { + std::string new_id = node.pin_id("as_lambda"); + for (auto& l : graph_.links) { + if (l.from_pin == node.lambda_grab.id) l.from_pin = new_id; + if (l.to_pin == node.lambda_grab.id) l.to_pin = new_id; + } + node.lambda_grab.id = new_id; + } + { + std::string new_id = node.pin_id("post_bang"); + for (auto& l : graph_.links) { + if (l.from_pin == node.bang_pin.id) l.from_pin = new_id; + if (l.to_pin == node.bang_pin.id) l.to_pin = new_id; + } + node.bang_pin.id = new_id; + } + + update_shadows_for_node(graph_, node, rest_args); + + editing_node_ = -1; + mark_dirty(); + } while (false); + + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { + if (creating_new_node_ && edit_node) { + graph_.remove_node(editing_node_); + } + creating_new_node_ = false; + editing_node_ = -1; + } + + ImGui::End(); + ImGui::PopStyleVar(3); + } // end of edit window block + } + + // --- Wire name editing popup --- + if (editing_link_ >= 0) { + FlowLink* edit_link = nullptr; + for (auto& link : graph_.links) { + if (link.id == editing_link_) { edit_link = &link; break; } + } + if (!edit_link) { + editing_link_ = -1; + } else { + ImVec2 fp = {}, tp = {}; + for (auto& n : graph_.nodes) { + for (auto& p : n.outputs) if (p->id == edit_link->from_pin) fp = get_pin_pos(n, *p, canvas_origin); + for (auto& p : n.nexts) if (p->id == edit_link->from_pin) fp = get_pin_pos(n, *p, canvas_origin); + if (n.lambda_grab.id == edit_link->from_pin) fp = get_pin_pos(n, n.lambda_grab, canvas_origin); + if (n.bang_pin.id == edit_link->from_pin) fp = get_pin_pos(n, n.bang_pin, canvas_origin); + for (auto& p : n.inputs) if (p->id == edit_link->to_pin) tp = get_pin_pos(n, *p, canvas_origin); + for (auto& p : n.triggers) if (p->id == edit_link->to_pin) tp = get_pin_pos(n, *p, canvas_origin); + } + ImVec2 mid = {(fp.x + tp.x) * 0.5f, (fp.y + tp.y) * 0.5f}; + + float text_w = ImGui::CalcTextSize(link_edit_buf_.c_str()).x * canvas_zoom_ + 40.0f * canvas_zoom_; + float popup_w = std::max(200.0f * canvas_zoom_, text_w); + ImGui::SetNextWindowPos({mid.x - popup_w * 0.5f, mid.y - 15.0f * canvas_zoom_}); + ImGui::SetNextWindowSize({popup_w, 0}); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {4 * canvas_zoom_, 4 * canvas_zoom_}); + ImGui::Begin("##wire_rename", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar); + ImGui::SetWindowFontScale(canvas_zoom_); + + bool was_just_opened = link_edit_just_opened_; + if (link_edit_just_opened_) { + ImGui::SetKeyboardFocusHere(); + link_edit_just_opened_ = false; + } + + char buf[128]; + strncpy(buf, link_edit_buf_.c_str(), sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + + bool committed = ImGui::InputText("##wire_name", buf, sizeof(buf), + ImGuiInputTextFlags_EnterReturnsTrue); + link_edit_buf_ = buf; + + bool valid = true; + std::string error_msg; + std::string new_name = link_edit_buf_; + if (new_name.empty() || new_name[0] != '$') { + valid = false; + error_msg = "Must start with $"; + } else if (new_name.size() < 2) { + valid = false; + error_msg = "Name too short"; + } else { + for (auto& other : graph_.links) { + if (other.id == edit_link->id) continue; + if (other.net_name == new_name && other.from_pin != edit_link->from_pin) { + valid = false; + error_msg = "Name already in use"; + break; + } + } + } + + if (!valid && !error_msg.empty()) { + ImGui::TextColored({1.0f, 0.3f, 0.3f, 1.0f}, "%s", error_msg.c_str()); + } + + if (committed && valid) { + std::string old_from = edit_link->from_pin; + for (auto& link : graph_.links) { + if (link.from_pin == old_from) { + link.net_name = new_name; + link.auto_wire = false; + } + } + editing_link_ = -1; + rebuild_all_inline_display(graph_); + mark_dirty(); + } + + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { + editing_link_ = -1; + } + + if (!was_just_opened && + !ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && + ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + editing_link_ = -1; + } + + ImGui::End(); + ImGui::PopStyleVar(1); + } + } +} diff --git a/src/attoflow/editor1.h b/src/attoflow/editor1.h new file mode 100644 index 0000000..11656e9 --- /dev/null +++ b/src/attoflow/editor1.h @@ -0,0 +1,135 @@ +#pragma once +#include "editor_pane.h" +#include "atto/model.h" +#include "atto/types.h" +#include "imgui.h" +#include +#include +#include +#include + +// Conversion between Vec2 (model) and ImVec2 (UI) +inline ImVec2 to_imvec(Vec2 v) { return {v.x, v.y}; } +inline Vec2 to_vec2(ImVec2 v) { return {v.x, v.y}; } + +class Editor1Pane : public IEditorPane { +public: + // IEditorPane + bool load(const std::string& path) override; + void draw() override; + bool is_loaded() const override { return !graph_.nodes.empty() || !file_path_.empty(); } + bool is_dirty() const override { return dirty_; } + const std::string& file_path() const override { return file_path_; } + const std::string& tab_name() const override { return tab_name_; } + + // Legacy graph access (for FlowEditorWindow toolbar/build) + FlowGraph& graph() { return graph_; } + + // Edit operations + void mark_dirty(); + void push_undo(); + void undo(); + void redo(); + void copy_selection(); + void paste_at(ImVec2 canvas_pos); + + // Debounced save + void schedule_save(); + void check_debounced_save(); + void auto_save(); + + // Validation & type inference + void validate_nodes(); + void run_type_inference(); + + // Navigation + void center_on_node(const FlowNode& node, ImVec2 canvas_size); + + // Viewport sync (call before save) + void sync_viewport(); + +private: + // Model + FlowGraph graph_; + std::string file_path_; + std::string tab_name_; + bool dirty_ = false; + + // Canvas + ImVec2 canvas_offset_ = {0, 0}; + float canvas_zoom_ = 1.0f; + + // Selection + std::set selected_nodes_; + + // Undo/Redo + std::vector undo_stack_; + std::vector redo_stack_; + + // Type inference + TypePool type_pool_; + bool inference_dirty_ = true; + + // Clipboard + struct ClipboardNode { + NodeTypeID type_id; std::string args; + ImVec2 offset; + }; + struct ClipboardLink { + int from_idx, to_idx; + std::string from_pin_name, to_pin_name; + }; + std::vector clipboard_nodes_; + std::vector clipboard_links_; + + // Highlight animation + int highlight_node_id_ = -1; + float highlight_timer_ = 0.0f; + + // Interaction state + int dragging_node_ = -1; + bool dragging_selection_ = false; + std::string dragging_link_from_pin_; + bool dragging_link_from_output_ = true; + ImVec2 dragging_link_start_; + bool canvas_dragging_ = false; + ImVec2 canvas_drag_start_; + + struct GrabbedLink { std::string from_pin; std::string to_pin; }; + std::vector grabbed_links_; + std::string grabbed_pin_; + bool grab_is_output_ = false; + bool grab_pending_ = false; + ImVec2 grab_start_; + + bool box_selecting_ = false; + ImVec2 box_select_start_; + + int editing_node_ = -1; + std::string edit_buf_; + bool edit_just_opened_ = false; + bool edit_cursor_to_end_ = false; + bool creating_new_node_ = false; + ImVec2 new_node_pos_; + + int editing_link_ = -1; + std::string link_edit_buf_; + bool link_edit_just_opened_ = false; + + std::set shadow_connected_pins_; + + // Debounced save + double save_deadline_ = 0; + + // Drawing helpers + ImVec2 canvas_to_screen(ImVec2 p, ImVec2 canvas_origin) const; + ImVec2 screen_to_canvas(ImVec2 p, ImVec2 canvas_origin) const; + ImVec2 get_pin_pos(const FlowNode& node, const FlowPin& pin, ImVec2 canvas_origin) const; + void draw_node(ImDrawList* dl, FlowNode& node, ImVec2 canvas_origin); + void draw_link(ImDrawList* dl, const FlowLink& link, ImVec2 canvas_origin); + + // Hit testing + struct PinHit { int node_id; std::string pin_id; FlowPin::Direction dir; }; + PinHit hit_test_pin(ImVec2 screen_pos, ImVec2 canvas_origin, float radius = 8.0f) const; + int hit_test_link(ImVec2 screen_pos, ImVec2 canvas_origin, float threshold = 6.0f) const; +}; diff --git a/src/attoflow/editor2.h b/src/attoflow/editor2.h index 775b6ff..b96de38 100644 --- a/src/attoflow/editor2.h +++ b/src/attoflow/editor2.h @@ -1,4 +1,5 @@ #pragma once +#include "editor_pane.h" #include "node_renderer.h" #include "atto/graph_editor_interfaces.h" #include @@ -72,15 +73,15 @@ struct ArgExprEditorImpl : IArgExprEditor { // ─── Editor2Pane ─── -class Editor2Pane : public IGraphEditor, public std::enable_shared_from_this { +class Editor2Pane : public IEditorPane, public IGraphEditor, public std::enable_shared_from_this { public: - bool load(const std::string& path); - void draw(); - - bool is_loaded() const { return gb_ != nullptr; } - bool is_dirty() const { return gb_ && gb_->is_dirty(); } - const std::string& file_path() const { return file_path_; } - const std::string& tab_name() const { return tab_name_; } + // IEditorPane + bool load(const std::string& path) override; + void draw() override; + bool is_loaded() const override { return gb_ != nullptr; } + bool is_dirty() const override { return gb_ && gb_->is_dirty(); } + const std::string& file_path() const override { return file_path_; } + const std::string& tab_name() const override { return tab_name_; } // IGraphEditor std::shared_ptr node_added(const NodeId& id, const std::shared_ptr& node) override; diff --git a/src/attoflow/editor_pane.h b/src/attoflow/editor_pane.h new file mode 100644 index 0000000..acc7eb3 --- /dev/null +++ b/src/attoflow/editor_pane.h @@ -0,0 +1,15 @@ +#pragma once +#include + +// Interface for editor panes (Editor1 legacy, Editor2 new graph-builder based) +struct IEditorPane { + virtual ~IEditorPane() = default; + + virtual bool load(const std::string& path) = 0; + virtual void draw() = 0; + + virtual bool is_loaded() const = 0; + virtual bool is_dirty() const = 0; + virtual const std::string& file_path() const = 0; + virtual const std::string& tab_name() const = 0; +}; diff --git a/src/attoflow/tab.h b/src/attoflow/tab.h new file mode 100644 index 0000000..57eeb0b --- /dev/null +++ b/src/attoflow/tab.h @@ -0,0 +1,14 @@ +#pragma once +#include "editor_pane.h" +#include +#include + +struct TabState { + std::shared_ptr pane; + + // Convenience accessors delegating to pane + bool is_loaded() const { return pane && pane->is_loaded(); } + bool is_dirty() const { return pane && pane->is_dirty(); } + const std::string& file_path() const { return pane->file_path(); } + const std::string& tab_name() const { return pane->tab_name(); } +}; From f296f08384cc369bcba79cbf986b3692a647a85f Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 22:52:43 +0200 Subject: [PATCH 78/86] editor->window.cpp/h --- CMakeLists.txt | 2 +- src/attoflow/main.cpp | 2 +- src/attoflow/{editor.cpp => window.cpp} | 2 +- src/attoflow/{editor.h => window.h} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename src/attoflow/{editor.cpp => window.cpp} (99%) rename src/attoflow/{editor.h => window.h} (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index ab2b2db..58e3f98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,7 +74,7 @@ if(ATTOLANG_BUILD_EDITOR) add_executable(attoflow src/attoflow/main.cpp - src/attoflow/editor.cpp + src/attoflow/window.cpp src/attoflow/editor1.cpp src/attoflow/editor2.cpp src/attoflow/node_renderer.cpp diff --git a/src/attoflow/main.cpp b/src/attoflow/main.cpp index 5e44c50..3676208 100644 --- a/src/attoflow/main.cpp +++ b/src/attoflow/main.cpp @@ -1,6 +1,6 @@ #include #include -#include "editor.h" +#include "window.h" int main(int argc, char* argv[]) { if (!SDL_Init(SDL_INIT_VIDEO)) { diff --git a/src/attoflow/editor.cpp b/src/attoflow/window.cpp similarity index 99% rename from src/attoflow/editor.cpp rename to src/attoflow/window.cpp index 17f9b1b..05930a9 100644 --- a/src/attoflow/editor.cpp +++ b/src/attoflow/window.cpp @@ -1,4 +1,4 @@ -#include "editor.h" +#include "window.h" #include "atto/args.h" #include "atto/serial.h" #include diff --git a/src/attoflow/editor.h b/src/attoflow/window.h similarity index 100% rename from src/attoflow/editor.h rename to src/attoflow/window.h From 9de4f0879fa77aaa8203472d9bf0c444ecd4518b Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 22:59:07 +0200 Subject: [PATCH 79/86] Remove / #if LEGACY_EDITOR editor1 --- CMakeLists.txt | 1 - src/attoflow/window.cpp | 26 ++++++++++++++++---------- src/attoflow/window.h | 1 - 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 58e3f98..05e04e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,7 +75,6 @@ if(ATTOLANG_BUILD_EDITOR) add_executable(attoflow src/attoflow/main.cpp src/attoflow/window.cpp - src/attoflow/editor1.cpp src/attoflow/editor2.cpp src/attoflow/node_renderer.cpp src/attoflow/tooltip_renderer.cpp diff --git a/src/attoflow/window.cpp b/src/attoflow/window.cpp index 05930a9..5e61800 100644 --- a/src/attoflow/window.cpp +++ b/src/attoflow/window.cpp @@ -68,17 +68,11 @@ void FlowEditorWindow::open_tab(const std::string& file_path) { std::shared_ptr pane; if (fs::exists(abs_path)) { - if (editor2->load(abs_path)) { - pane = editor2; - } else { - // Fallback to legacy Editor1Pane - auto editor1 = std::make_shared(); - editor1->load(abs_path); - pane = editor1; + if (!editor2->load(abs_path)) { + throw std::invalid_argument("Cannot load " + abs_path); } - } else { - pane = editor2; } + pane = editor2; TabState tab; tab.pane = pane; @@ -88,6 +82,7 @@ void FlowEditorWindow::open_tab(const std::string& file_path) { void FlowEditorWindow::close_tab(int idx) { if (idx < 0 || idx >= (int)tabs_.size()) return; + #if LEGACY_EDITOR // Auto-save before closing (Editor1Pane handles its own save) if (auto e1 = std::dynamic_pointer_cast(tabs_[idx].pane)) { if (e1->is_dirty() && !e1->file_path().empty()) { @@ -95,6 +90,7 @@ void FlowEditorWindow::close_tab(int idx) { e1->auto_save(); } } + #endif tabs_.erase(tabs_.begin() + idx); if (active_tab_ >= (int)tabs_.size()) active_tab_ = std::max(0, (int)tabs_.size() - 1); @@ -218,10 +214,11 @@ void FlowEditorWindow::draw() { ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS); // --- Bottom panel --- - auto* e1 = dynamic_cast(active().pane.get()); + // auto* e1 = dynamic_cast(active().pane.get()); ImGui::BeginChild("##bottom_panel", {canvas_w, bottom_panel_height_}, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); if (ImGui::BeginTabBar("##bottom_tabs")) { +#if LEGACY_EDITOR if (e1) { int error_count = 0; for (auto& node : e1->graph().nodes) if (!node.error.empty()) error_count++; @@ -261,6 +258,7 @@ void FlowEditorWindow::draw() { ImGui::EndTabItem(); } } +#endif if (ImGui::BeginTabItem("Build Log", nullptr, show_build_log_ ? ImGuiTabItemFlags_SetSelected : 0)) { show_build_log_ = false; @@ -297,6 +295,7 @@ void FlowEditorWindow::draw() { // --- Side panel: declarations --- ImGui::BeginChild("##side_panel", {side_panel_width_, total_h}, true); +#if LEGACY_EDITOR if (e1) { ImGui::TextUnformatted("Declarations"); ImGui::Separator(); @@ -335,12 +334,15 @@ void FlowEditorWindow::draw() { } } } +#endif ImGui::EndChild(); ImGui::End(); // main +#if LEGACY_EDITOR // Check debounced save for Editor1Pane if (e1) e1->check_debounced_save(); +#endif win_.end_frame(30, 30, 40); } @@ -371,6 +373,7 @@ void FlowEditorWindow::draw_toolbar() { ImGui::SetNextItemWidth(120); if (ImGui::InputTextWithHint("##search", "Find node...", search_buf_, sizeof(search_buf_), ImGuiInputTextFlags_EnterReturnsTrue)) { + #if LEGACY_EDITOR if (auto* e1 = dynamic_cast(active().pane.get())) { std::string query(search_buf_); if (!query.empty()) { @@ -384,6 +387,7 @@ void FlowEditorWindow::draw_toolbar() { } } } + #endif } ImGui::SameLine(); @@ -408,10 +412,12 @@ void FlowEditorWindow::run_program(bool release) { build_log_.clear(); } +#if LEGACY_EDITOR // Auto-save via Editor1Pane if applicable if (auto* e1 = dynamic_cast(active().pane.get())) { e1->auto_save(); } +#endif std::string active_path = active().pane ? active().file_path() : ""; if (active_path.empty()) return; diff --git a/src/attoflow/window.h b/src/attoflow/window.h index 1e58daa..dcf6bf7 100644 --- a/src/attoflow/window.h +++ b/src/attoflow/window.h @@ -1,7 +1,6 @@ #pragma once #include "sdl_imgui_window.h" #include "tab.h" -#include "editor1.h" #include "editor2.h" #include #include From a53d8a575365fe8b405ef17b5c492503f5d9945e Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 23:03:34 +0200 Subject: [PATCH 80/86] shift/alt + scroll = x/y pan --- src/attoflow/editor2.cpp | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 77f9472..1531f55 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -402,17 +402,27 @@ void Editor2Pane::draw() { canvas_offset_.y += ImGui::GetIO().MouseDelta.y; } - // Zoom + // Scroll: zoom, or pan with modifiers if (canvas_hovered) { float wheel = ImGui::GetIO().MouseWheel; if (wheel != 0) { - float old_zoom = canvas_zoom_; - canvas_zoom_ *= (wheel > 0) ? 1.1f : 0.9f; - canvas_zoom_ = std::clamp(canvas_zoom_, 0.1f, 10.0f); - ImVec2 mouse = ImGui::GetIO().MousePos; - ImVec2 mouse_rel = v2sub(v2sub(mouse, canvas_p0), canvas_offset_); - ImVec2 mouse_canvas = v2mul(mouse_rel, 1.0f / old_zoom); - canvas_offset_ = v2sub(v2sub(mouse, canvas_p0), v2mul(mouse_canvas, canvas_zoom_)); + bool shift = ImGui::GetIO().KeyShift; + bool alt = ImGui::GetIO().KeyAlt; + if (shift || alt) { + // Shift+scroll = X pan, Alt+scroll = Y pan + float pan_speed = 40.0f; + if (shift) canvas_offset_.x += wheel * pan_speed; + if (alt) canvas_offset_.y += wheel * pan_speed; + } else { + // Plain scroll = zoom toward mouse + float old_zoom = canvas_zoom_; + canvas_zoom_ *= (wheel > 0) ? 1.1f : 0.9f; + canvas_zoom_ = std::clamp(canvas_zoom_, 0.1f, 10.0f); + ImVec2 mouse = ImGui::GetIO().MousePos; + ImVec2 mouse_rel = v2sub(v2sub(mouse, canvas_p0), canvas_offset_); + ImVec2 mouse_canvas = v2mul(mouse_rel, 1.0f / old_zoom); + canvas_offset_ = v2sub(v2sub(mouse, canvas_p0), v2mul(mouse_canvas, canvas_zoom_)); + } } } } From 7dd36564d9dc6f939f68220c75f464027f9bdde4 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 23:40:55 +0200 Subject: [PATCH 81/86] further decoulping refactors --- CMakeLists.txt | 1 + src/attoflow/atto_editor_shared_state.h | 7 + src/attoflow/editor2.cpp | 316 ++++++------------------ src/attoflow/editor2.h | 56 ++--- src/attoflow/editor_pane.h | 14 +- src/attoflow/tab.h | 17 +- src/attoflow/visual_editor.cpp | 154 ++++++++++++ src/attoflow/visual_editor.h | 71 ++++++ src/attoflow/window.cpp | 46 ++-- 9 files changed, 378 insertions(+), 304 deletions(-) create mode 100644 src/attoflow/atto_editor_shared_state.h create mode 100644 src/attoflow/visual_editor.cpp create mode 100644 src/attoflow/visual_editor.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 05e04e6..6a41e78 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,7 @@ if(ATTOLANG_BUILD_EDITOR) src/attoflow/main.cpp src/attoflow/window.cpp src/attoflow/editor2.cpp + src/attoflow/visual_editor.cpp src/attoflow/node_renderer.cpp src/attoflow/tooltip_renderer.cpp src/attoflow/editor_style.cpp diff --git a/src/attoflow/atto_editor_shared_state.h b/src/attoflow/atto_editor_shared_state.h new file mode 100644 index 0000000..3302cdf --- /dev/null +++ b/src/attoflow/atto_editor_shared_state.h @@ -0,0 +1,7 @@ +#pragma once +#include "atto/graph_builder.h" +#include + +struct AttoEditorSharedState { + std::set selected_nodes; +}; diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 1531f55..896dea9 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -1,5 +1,4 @@ #include "editor2.h" -#include "node_renderer.h" #include "tooltip_renderer.h" #include "atto/graph_builder.h" #include "atto/node_types2.h" @@ -10,6 +9,21 @@ #include #include +// ─── Factory ─── + +Editor2Pane::Editor2Pane(const std::shared_ptr& gb, + const std::shared_ptr& shared) + : VisualEditor(shared), gb_(gb) { +} + +std::shared_ptr make_editor2( + const std::shared_ptr& gb, + const std::shared_ptr& shared) { + auto pane = std::make_shared(gb, shared); + gb->add_editor(pane); + return pane; +} + // ─── Per-item editor implementations ─── void NodeEditorImpl::rebuild(ImVec2 canvas_origin, float zoom) { @@ -78,33 +92,6 @@ void Editor2Pane::net_removed(const NodeId& id) { wires_dirty_ = true; } -// ─── Load ─── - -bool Editor2Pane::load(const std::string& path) { - std::ifstream f(path); - if (!f.is_open()) { - fprintf(stderr, "Editor2: cannot open %s\n", path.c_str()); - return false; - } - - auto result = Deserializer::parse_atto(f); - if (auto* err = std::get_if(&result)) { - fprintf(stderr, "Editor2: %s\n", err->c_str()); - return false; - } - - gb_ = std::get>(result); - file_path_ = path; - - auto slash = path.find_last_of("/\\"); - tab_name_ = (slash != std::string::npos) ? path.substr(slash + 1) : path; - - gb_->add_editor(shared_from_this()); - - printf("Editor2: loaded %zu entries from %s\n", gb_->entries.size(), path.c_str()); - return true; -} - // ─── Wire rebuilding ─── void Editor2Pane::rebuild_wires(ImVec2 canvas_origin) { @@ -119,7 +106,6 @@ void Editor2Pane::rebuild_wires(ImVec2 canvas_origin) { auto& dst_layout = ned->layout; auto& dst_vpm = ned->vpm; - // For each input pin with a net connection, compute wire geometry for (int i = 0; i < (int)dst_vpm.inputs.size(); i++) { auto& pin = dst_vpm.inputs[i]; if (pin.kind == VisualPinKind::AddDiamond || pin.kind == VisualPinKind::AbsentOptional) continue; @@ -139,22 +125,15 @@ void Editor2Pane::rebuild_wires(ImVec2 canvas_origin) { auto src_ptr = net->source().lock(); src_node = src_ptr ? src_ptr->as_node() : nullptr; if (!src_node) continue; - // Find which output pin sources this net for (int k = 0; k < (int)src_node->outputs.size(); k++) { auto out_net = src_node->outputs[k]->as_net(); - if (out_net && out_net->second() == entry) { - source_pin = k; - break; - } + if (out_net && out_net->second() == entry) { source_pin = k; break; } } if (source_pin == 0) { int base = (int)src_node->outputs.size(); for (int k = 0; k < (int)src_node->outputs_va_args.size(); k++) { auto out_net = src_node->outputs_va_args[k]->as_net(); - if (out_net && out_net->second() == entry) { - source_pin = base + k; - break; - } + if (out_net && out_net->second() == entry) { source_pin = base + k; break; } } } } else if (auto node = entry->as_node()) { @@ -177,11 +156,9 @@ void Editor2Pane::rebuild_wires(ImVec2 canvas_origin) { is_side_bang = src_nt && src_nt->is_flow() && source_pin < (src_nt->num_outputs) && src_nt->output_ports && src_nt->output_ports[source_pin].kind == PortKind2::BangNext; - if (is_side_bang) { from = src_layout.side_bang_pos(); } else { - // Map source_pin to visual output index (skip side-bang for flow) int visual_pin = source_pin; if (src_nt && src_nt->is_flow()) visual_pin = std::max(0, visual_pin - 1); from = src_layout.output_pin_pos(visual_pin); @@ -201,36 +178,24 @@ void Editor2Pane::rebuild_wires(ImVec2 canvas_origin) { void Editor2Pane::draw() { if (!gb_) { - ImGui::TextDisabled("No file loaded"); + ImGui::TextDisabled("No graph loaded"); return; } + draw_canvas("##canvas2"); +} - ImVec2 canvas_p0 = ImGui::GetCursorScreenPos(); - ImVec2 canvas_sz = ImGui::GetContentRegionAvail(); - if (canvas_sz.x < 50.0f) canvas_sz.x = 50.0f; - if (canvas_sz.y < 50.0f) canvas_sz.y = 50.0f; - - ImGui::InvisibleButton("##canvas2", canvas_sz, - ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight); - bool canvas_hovered = ImGui::IsItemHovered(); - - ImDrawList* dl = ImGui::GetWindowDrawList(); - ImVec2 canvas_origin = v2add(canvas_p0, canvas_offset_); - - render_background(dl, canvas_p0, canvas_sz, canvas_offset_, canvas_zoom_); - - dl->PushClipRect(canvas_p0, v2add(canvas_p0, canvas_sz), true); +// ─── VisualEditor hooks ─── +void Editor2Pane::draw_content(const CanvasFrame& frame) { // Rebuild node layouts and draw nodes for (auto& [id, ned] : node_editors_) { if (ned->node->shadow) throw std::logic_error("Editor2Pane: shadow nodes must be folded before rendering (id: " + id + ")"); - ned->rebuild(canvas_origin, canvas_zoom_); + ned->rebuild(frame.canvas_origin, canvas_zoom_); - // Build render state from editor interaction state auto& node = ned->node; NodeRenderState state; - state.selected = selected_nodes_.count(node) > 0; + state.selected = shared_ && shared_->selected_nodes.count(node) > 0; state.node_hovered = false; if (auto* ep = std::get_if(&hover_item_)) state.node_hovered = (*ep == node); @@ -245,192 +210,21 @@ void Editor2Pane::draw() { state.add_pin_hover = std::get_if(&hover_item_); auto* nt = find_node_type2(node->type_id); - render_node(dl, node, nt, ned->layout, ned->vpm, ned->display_text, + render_node(frame.dl, node, nt, ned->layout, ned->vpm, ned->display_text, state, canvas_zoom_, draw_tooltips_); } // Rebuild wires - rebuild_wires(canvas_origin); + rebuild_wires(frame.canvas_origin); // Draw wires for (auto& w : cached_wires_) { - render_wire(dl, w, canvas_zoom_); - render_wire_label(dl, w, canvas_zoom_); - } - - dl->PopClipRect(); - - // ─── Hover detection + effects ─── - if (canvas_hovered) { - ImVec2 mouse = ImGui::GetIO().MousePos; - hover_item_ = detect_hover(mouse, canvas_origin); - } else { - hover_item_ = std::monostate{}; - } - draw_hover_effects(dl, canvas_origin, hover_item_); - - // Extract hover node from variant - FlowNodeBuilderPtr hover_node = nullptr; - if (auto* ep = std::get_if(&hover_item_)) { - if (*ep) hover_node = (*ep)->as_node(); - } else if (auto* pin = std::get_if(&hover_item_)) { - hover_node = (*pin)->node(); - } - - // ─── Selection + dragging with left mouse ─── - if (canvas_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { - bool ctrl = ImGui::GetIO().KeyCtrl; - - if (ctrl && hover_node) { - if (selected_nodes_.count(hover_node)) - selected_nodes_.erase(hover_node); - else - selected_nodes_.insert(hover_node); - } else if (hover_node) { - if (!selected_nodes_.count(hover_node)) { - selected_nodes_.clear(); - selected_nodes_.insert(hover_node); - } - dragging_started_ = true; - - drag_was_overlapping_ = false; - float pad = S.node_height * 0.5f; - for (auto& sel : selected_nodes_) { - auto sel_vpm = VisualPinMap::build(sel, find_node_type2(sel->type_id)); - auto sel_layout = compute_node_layout(sel, sel_vpm, {0,0}, 1.0f); - for (auto& [oid, oned] : node_editors_) { - auto on = oned->node; - if (selected_nodes_.count(on)) continue; - auto on_vpm = VisualPinMap::build(on, find_node_type2(on->type_id)); - auto ol = compute_node_layout(on, on_vpm, {0,0}, 1.0f); - if (sel->position.x < on->position.x - pad + ol.width + pad * 2 && - sel->position.x + sel_layout.width > on->position.x - pad && - sel->position.y < on->position.y - pad + ol.height + pad * 2 && - sel->position.y + sel_layout.height > on->position.y - pad) { - drag_was_overlapping_ = true; - break; - } - } - if (drag_was_overlapping_) break; - } - } else { - selected_nodes_.clear(); - selection_rect_active_ = true; - ImVec2 mouse = ImGui::GetIO().MousePos; - selection_rect_start_ = {(mouse.x - canvas_origin.x) / canvas_zoom_, - (mouse.y - canvas_origin.y) / canvas_zoom_}; - } - } - - // Drag all selected nodes - if (dragging_started_ && ImGui::IsMouseDragging(ImGuiMouseButton_Left) && !selected_nodes_.empty()) { - ImVec2 delta = ImGui::GetIO().MouseDelta; - float dx = delta.x / canvas_zoom_; - float dy = delta.y / canvas_zoom_; - - bool blocked = false; - if (!drag_was_overlapping_) { - float pad = S.node_height * 0.5f; - for (auto& sel : selected_nodes_) { - auto sel_vpm = VisualPinMap::build(sel, find_node_type2(sel->type_id)); - auto sel_layout = compute_node_layout(sel, sel_vpm, {0,0}, 1.0f); - float nx = sel->position.x + dx, ny = sel->position.y + dy; - for (auto& [oid, oned] : node_editors_) { - auto on = oned->node; - if (selected_nodes_.count(on)) continue; - auto on_vpm = VisualPinMap::build(on, find_node_type2(on->type_id)); - auto ol = compute_node_layout(on, on_vpm, {0,0}, 1.0f); - float ox = on->position.x - pad, oy = on->position.y - pad; - float ow = ol.width + pad * 2, oh = ol.height + pad * 2; - if (nx < ox + ow && nx + sel_layout.width > ox && - ny < oy + oh && ny + sel_layout.height > oy) { - blocked = true; - break; - } - } - if (blocked) break; - } - } - if (!blocked) { - for (auto& sel : selected_nodes_) { - sel->position.x += dx; - sel->position.y += dy; - } - wires_dirty_ = true; - } - } - - // Selection rectangle - if (selection_rect_active_) { - ImVec2 mouse = ImGui::GetIO().MousePos; - ImVec2 cur_canvas = {(mouse.x - canvas_origin.x) / canvas_zoom_, - (mouse.y - canvas_origin.y) / canvas_zoom_}; - - float x0 = std::min(selection_rect_start_.x, cur_canvas.x); - float y0 = std::min(selection_rect_start_.y, cur_canvas.y); - float x1 = std::max(selection_rect_start_.x, cur_canvas.x); - float y1 = std::max(selection_rect_start_.y, cur_canvas.y); - - ImVec2 sp0 = {canvas_origin.x + x0 * canvas_zoom_, canvas_origin.y + y0 * canvas_zoom_}; - ImVec2 sp1 = {canvas_origin.x + x1 * canvas_zoom_, canvas_origin.y + y1 * canvas_zoom_}; - render_selection_rect(dl, sp0, sp1); - - selected_nodes_.clear(); - for (auto& [id, ned] : node_editors_) { - auto node = ned->node; - auto node_vpm = VisualPinMap::build(node, find_node_type2(node->type_id)); - auto layout = compute_node_layout(node, node_vpm, {0,0}, 1.0f); - float nx0 = node->position.x, ny0 = node->position.y; - float nx1 = nx0 + layout.width, ny1 = ny0 + layout.height; - if (nx0 < x1 && nx1 > x0 && ny0 < y1 && ny1 > y0) - selected_nodes_.insert(node); - } - } - - if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { - dragging_started_ = false; - selection_rect_active_ = false; - } - - // Pan - if (canvas_hovered && ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) { - canvas_offset_.x += ImGui::GetIO().MouseDelta.x; - canvas_offset_.y += ImGui::GetIO().MouseDelta.y; - } - if (canvas_hovered && ImGui::IsMouseDragging(ImGuiMouseButton_Right)) { - canvas_offset_.x += ImGui::GetIO().MouseDelta.x; - canvas_offset_.y += ImGui::GetIO().MouseDelta.y; - } - - // Scroll: zoom, or pan with modifiers - if (canvas_hovered) { - float wheel = ImGui::GetIO().MouseWheel; - if (wheel != 0) { - bool shift = ImGui::GetIO().KeyShift; - bool alt = ImGui::GetIO().KeyAlt; - if (shift || alt) { - // Shift+scroll = X pan, Alt+scroll = Y pan - float pan_speed = 40.0f; - if (shift) canvas_offset_.x += wheel * pan_speed; - if (alt) canvas_offset_.y += wheel * pan_speed; - } else { - // Plain scroll = zoom toward mouse - float old_zoom = canvas_zoom_; - canvas_zoom_ *= (wheel > 0) ? 1.1f : 0.9f; - canvas_zoom_ = std::clamp(canvas_zoom_, 0.1f, 10.0f); - ImVec2 mouse = ImGui::GetIO().MousePos; - ImVec2 mouse_rel = v2sub(v2sub(mouse, canvas_p0), canvas_offset_); - ImVec2 mouse_canvas = v2mul(mouse_rel, 1.0f / old_zoom); - canvas_offset_ = v2sub(v2sub(mouse, canvas_p0), v2mul(mouse_canvas, canvas_zoom_)); - } - } + render_wire(frame.dl, w, canvas_zoom_); + render_wire_label(frame.dl, w, canvas_zoom_); } } -// ─── Hover detection ─── - -HoverItem Editor2Pane::detect_hover(ImVec2 mouse, ImVec2 canvas_origin) { - // Build hit targets from node editors +HoverItem Editor2Pane::do_detect_hover(ImVec2 mouse, ImVec2 canvas_origin) { std::vector targets; targets.reserve(node_editors_.size()); for (auto& [id, ned] : node_editors_) { @@ -439,12 +233,10 @@ HoverItem Editor2Pane::detect_hover(ImVec2 mouse, ImVec2 canvas_origin) { targets.push_back({ned->node, nt, &ned->layout, &ned->vpm}); } - // Test all 3 categories auto wire_hit = hit_test_wires(mouse, cached_wires_, canvas_zoom_); auto node_hit = hit_test_node_bodies(mouse, targets, canvas_zoom_); auto pin_hit = hit_test_pins(mouse, targets, canvas_zoom_); - // Pick winner (pins have bias built into their distance) HitResult best = wire_hit; if (node_hit.distance < best.distance) best = node_hit; if (pin_hit.distance < best.distance) best = pin_hit; @@ -452,9 +244,7 @@ HoverItem Editor2Pane::detect_hover(ImVec2 mouse, ImVec2 canvas_origin) { return best.item; } -// ─── Hover effects ─── - -void Editor2Pane::draw_hover_effects(ImDrawList* dl, ImVec2 canvas_origin, const HoverItem& hover) { +void Editor2Pane::do_draw_hover_effects(ImDrawList* dl, ImVec2 canvas_origin, const HoverItem& hover) { if (std::holds_alternative(hover)) return; FlowNodeBuilderPtr hover_node = nullptr; @@ -465,20 +255,16 @@ void Editor2Pane::draw_hover_effects(ImDrawList* dl, ImVec2 canvas_origin, const hover_node = hover_entry ? hover_entry->as_node() : nullptr; } - // Node hovered: highlight lambda wires capturing it if (hover_node) { - for (auto& w : cached_wires_) { + for (auto& w : cached_wires_) if (w.is_lambda() && w.entry() == hover_node) render_wire_highlight(dl, w, canvas_zoom_); - } } - // Wire/net hovered: highlight all wires in the same net if (hover_entry && hover_entry->as_net()) { - for (auto& w : cached_wires_) { + for (auto& w : cached_wires_) if (w.entry() == hover_entry) render_wire_highlight(dl, w, canvas_zoom_); - } if (draw_tooltips_) { for (auto& w : cached_wires_) { if (w.entry() == hover_entry) { @@ -489,3 +275,41 @@ void Editor2Pane::draw_hover_effects(ImDrawList* dl, ImVec2 canvas_origin, const } } } + +FlowNodeBuilderPtr Editor2Pane::hover_to_node(const HoverItem& item) { + if (auto* ep = std::get_if(&item)) { + if (*ep) return (*ep)->as_node(); + } else if (auto* pin = std::get_if(&item)) { + return (*pin)->node(); + } + return nullptr; +} + +bool Editor2Pane::test_drag_overlap(const FlowNodeBuilderPtr& sel, float nx, float ny) { + float pad = S.node_height * 0.5f; + auto sel_vpm = VisualPinMap::build(sel, find_node_type2(sel->type_id)); + auto sel_layout = compute_node_layout(sel, sel_vpm, {0,0}, 1.0f); + for (auto& [oid, oned] : node_editors_) { + auto on = oned->node; + if (shared_ && shared_->selected_nodes.count(on)) continue; + auto on_vpm = VisualPinMap::build(on, find_node_type2(on->type_id)); + auto ol = compute_node_layout(on, on_vpm, {0,0}, 1.0f); + float ox = on->position.x - pad, oy = on->position.y - pad; + float ow = ol.width + pad * 2, oh = ol.height + pad * 2; + if (nx < ox + ow && nx + sel_layout.width > ox && + ny < oy + oh && ny + sel_layout.height > oy) + return true; + } + return false; +} + +std::vector Editor2Pane::get_box_test_nodes() { + std::vector result; + for (auto& [id, ned] : node_editors_) { + auto node = ned->node; + auto vpm = VisualPinMap::build(node, find_node_type2(node->type_id)); + auto layout = compute_node_layout(node, vpm, {0,0}, 1.0f); + result.push_back({node, node->position.x, node->position.y, layout.width, layout.height}); + } + return result; +} diff --git a/src/attoflow/editor2.h b/src/attoflow/editor2.h index b96de38..7c9bce7 100644 --- a/src/attoflow/editor2.h +++ b/src/attoflow/editor2.h @@ -1,10 +1,9 @@ #pragma once #include "editor_pane.h" -#include "node_renderer.h" +#include "visual_editor.h" #include "atto/graph_editor_interfaces.h" #include #include -#include #include // ─── Forward declaration ─── @@ -25,7 +24,6 @@ struct NodeEditorImpl : INodeEditor, std::enable_shared_from_this& node) override; void node_layout_changed(const std::shared_ptr& node) override; std::shared_ptr create_arg_net_editor(const std::shared_ptr& arg) override; @@ -37,9 +35,7 @@ struct NodeEditorImpl : INodeEditor, std::enable_shared_from_this& net) override; }; @@ -73,17 +69,18 @@ struct ArgExprEditorImpl : IArgExprEditor { // ─── Editor2Pane ─── -class Editor2Pane : public IEditorPane, public IGraphEditor, public std::enable_shared_from_this { +class Editor2Pane : public IEditorPane, public VisualEditor, + public IGraphEditor, public std::enable_shared_from_this { public: + Editor2Pane(const std::shared_ptr& gb, + const std::shared_ptr& shared); + // IEditorPane - bool load(const std::string& path) override; void draw() override; - bool is_loaded() const override { return gb_ != nullptr; } - bool is_dirty() const override { return gb_ && gb_->is_dirty(); } - const std::string& file_path() const override { return file_path_; } - const std::string& tab_name() const override { return tab_name_; } + const char* type_name() const override { return "graph"; } + std::shared_ptr get_graph_builder() const override { return gb_; } - // IGraphEditor + // IGraphEditor (observer) std::shared_ptr node_added(const NodeId& id, const std::shared_ptr& node) override; void node_removed(const NodeId& id) override; std::shared_ptr net_added(const NodeId& id, const std::shared_ptr& net) override; @@ -91,23 +88,21 @@ class Editor2Pane : public IEditorPane, public IGraphEditor, public std::enable_ void invalidate_wires() { wires_dirty_ = true; } +protected: + // VisualEditor hooks + void draw_content(const CanvasFrame& frame) override; + HoverItem do_detect_hover(ImVec2 mouse, ImVec2 canvas_origin) override; + void do_draw_hover_effects(ImDrawList* dl, ImVec2 canvas_origin, const HoverItem& hover) override; + FlowNodeBuilderPtr hover_to_node(const HoverItem& item) override; + bool test_drag_overlap(const FlowNodeBuilderPtr& sel, float nx, float ny) override; + std::vector get_box_test_nodes() override; + void on_nodes_moved() override { wires_dirty_ = true; } + private: + friend struct NodeEditorImpl; + friend struct NetEditorImpl; + std::shared_ptr gb_; - std::string file_path_; - std::string tab_name_; - - // Canvas state - ImVec2 canvas_offset_ = {0, 0}; - float canvas_zoom_ = 1.0f; - - // Interaction state - HoverItem hover_item_; - bool draw_tooltips_ = true; - std::set selected_nodes_; - bool dragging_started_ = false; - bool drag_was_overlapping_ = false; - bool selection_rect_active_ = false; - ImVec2 selection_rect_start_ = {0, 0}; // Per-item editor caches std::map> node_editors_; @@ -118,6 +113,9 @@ class Editor2Pane : public IEditorPane, public IGraphEditor, public std::enable_ bool wires_dirty_ = true; void rebuild_wires(ImVec2 canvas_origin); - HoverItem detect_hover(ImVec2 mouse, ImVec2 canvas_origin); - void draw_hover_effects(ImDrawList* dl, ImVec2 canvas_origin, const HoverItem& hover); }; + +// Factory +std::shared_ptr make_editor2( + const std::shared_ptr& gb, + const std::shared_ptr& shared); diff --git a/src/attoflow/editor_pane.h b/src/attoflow/editor_pane.h index acc7eb3..5554bad 100644 --- a/src/attoflow/editor_pane.h +++ b/src/attoflow/editor_pane.h @@ -1,15 +1,13 @@ #pragma once -#include +#include -// Interface for editor panes (Editor1 legacy, Editor2 new graph-builder based) +struct GraphBuilder; + +// Interface for editor panes — views into a GraphBuilder struct IEditorPane { virtual ~IEditorPane() = default; - virtual bool load(const std::string& path) = 0; virtual void draw() = 0; - - virtual bool is_loaded() const = 0; - virtual bool is_dirty() const = 0; - virtual const std::string& file_path() const = 0; - virtual const std::string& tab_name() const = 0; + virtual const char* type_name() const = 0; + virtual std::shared_ptr get_graph_builder() const = 0; }; diff --git a/src/attoflow/tab.h b/src/attoflow/tab.h index 57eeb0b..f18bad8 100644 --- a/src/attoflow/tab.h +++ b/src/attoflow/tab.h @@ -1,14 +1,21 @@ #pragma once #include "editor_pane.h" +#include "atto_editor_shared_state.h" +#include "atto/graph_builder.h" #include #include struct TabState { + std::shared_ptr gb; + std::shared_ptr shared; std::shared_ptr pane; + std::string file_path; + std::string tab_name; - // Convenience accessors delegating to pane - bool is_loaded() const { return pane && pane->is_loaded(); } - bool is_dirty() const { return pane && pane->is_dirty(); } - const std::string& file_path() const { return pane->file_path(); } - const std::string& tab_name() const { return pane->tab_name(); } + std::string label() const { + std::string l = tab_name; + if (pane) l += std::string("[") + pane->type_name() + "]"; + if (gb && gb->is_dirty()) l += "*"; + return l; + } }; diff --git a/src/attoflow/visual_editor.cpp b/src/attoflow/visual_editor.cpp new file mode 100644 index 0000000..97548ae --- /dev/null +++ b/src/attoflow/visual_editor.cpp @@ -0,0 +1,154 @@ +#include "visual_editor.h" +#include +#include + +void VisualEditor::draw_canvas(const char* id) { + ImVec2 canvas_p0 = ImGui::GetCursorScreenPos(); + ImVec2 canvas_sz = ImGui::GetContentRegionAvail(); + if (canvas_sz.x < 50.0f) canvas_sz.x = 50.0f; + if (canvas_sz.y < 50.0f) canvas_sz.y = 50.0f; + + ImGui::InvisibleButton(id, canvas_sz, + ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight); + bool canvas_hovered = ImGui::IsItemHovered(); + + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec2 canvas_origin = v2add(canvas_p0, canvas_offset_); + + render_background(dl, canvas_p0, canvas_sz, canvas_offset_, canvas_zoom_); + + dl->PushClipRect(canvas_p0, v2add(canvas_p0, canvas_sz), true); + + CanvasFrame frame{dl, canvas_p0, canvas_sz, canvas_origin, canvas_hovered}; + draw_content(frame); + + dl->PopClipRect(); + + // ─── Hover detection + effects ─── + if (canvas_hovered) { + ImVec2 mouse = ImGui::GetIO().MousePos; + hover_item_ = do_detect_hover(mouse, canvas_origin); + } else { + hover_item_ = std::monostate{}; + } + do_draw_hover_effects(dl, canvas_origin, hover_item_); + + // Extract hover node + FlowNodeBuilderPtr hover_node = hover_to_node(hover_item_); + + // ─── Selection + dragging with left mouse ─── + if (canvas_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + bool ctrl = ImGui::GetIO().KeyCtrl; + + if (ctrl && hover_node) { + if (shared_->selected_nodes.count(hover_node)) + shared_->selected_nodes.erase(hover_node); + else + shared_->selected_nodes.insert(hover_node); + } else if (hover_node) { + if (!shared_->selected_nodes.count(hover_node)) { + shared_->selected_nodes.clear(); + shared_->selected_nodes.insert(hover_node); + } + dragging_started_ = true; + + // Check initial overlap + drag_was_overlapping_ = false; + for (auto& sel : shared_->selected_nodes) { + if (test_drag_overlap(sel, sel->position.x, sel->position.y)) { + drag_was_overlapping_ = true; + break; + } + } + } else { + shared_->selected_nodes.clear(); + selection_rect_active_ = true; + ImVec2 mouse = ImGui::GetIO().MousePos; + selection_rect_start_ = {(mouse.x - canvas_origin.x) / canvas_zoom_, + (mouse.y - canvas_origin.y) / canvas_zoom_}; + } + } + + // Drag selected nodes + if (dragging_started_ && ImGui::IsMouseDragging(ImGuiMouseButton_Left) && !shared_->selected_nodes.empty()) { + ImVec2 delta = ImGui::GetIO().MouseDelta; + float dx = delta.x / canvas_zoom_; + float dy = delta.y / canvas_zoom_; + + bool blocked = false; + if (!drag_was_overlapping_) { + for (auto& sel : shared_->selected_nodes) { + if (test_drag_overlap(sel, sel->position.x + dx, sel->position.y + dy)) { + blocked = true; + break; + } + } + } + if (!blocked) { + for (auto& sel : shared_->selected_nodes) { + sel->position.x += dx; + sel->position.y += dy; + } + on_nodes_moved(); + } + } + + // Selection rectangle + if (selection_rect_active_) { + ImVec2 mouse = ImGui::GetIO().MousePos; + ImVec2 cur_canvas = {(mouse.x - canvas_origin.x) / canvas_zoom_, + (mouse.y - canvas_origin.y) / canvas_zoom_}; + + float x0 = std::min(selection_rect_start_.x, cur_canvas.x); + float y0 = std::min(selection_rect_start_.y, cur_canvas.y); + float x1 = std::max(selection_rect_start_.x, cur_canvas.x); + float y1 = std::max(selection_rect_start_.y, cur_canvas.y); + + ImVec2 sp0 = {canvas_origin.x + x0 * canvas_zoom_, canvas_origin.y + y0 * canvas_zoom_}; + ImVec2 sp1 = {canvas_origin.x + x1 * canvas_zoom_, canvas_origin.y + y1 * canvas_zoom_}; + render_selection_rect(dl, sp0, sp1); + + shared_->selected_nodes.clear(); + for (auto& btn : get_box_test_nodes()) { + if (btn.x < x1 && btn.x + btn.w > x0 && btn.y < y1 && btn.y + btn.h > y0) + shared_->selected_nodes.insert(btn.node); + } + } + + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + dragging_started_ = false; + selection_rect_active_ = false; + } + + // Pan + if (canvas_hovered && ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) { + canvas_offset_.x += ImGui::GetIO().MouseDelta.x; + canvas_offset_.y += ImGui::GetIO().MouseDelta.y; + } + if (canvas_hovered && ImGui::IsMouseDragging(ImGuiMouseButton_Right)) { + canvas_offset_.x += ImGui::GetIO().MouseDelta.x; + canvas_offset_.y += ImGui::GetIO().MouseDelta.y; + } + + // Scroll: zoom, or pan with modifiers + if (canvas_hovered) { + float wheel = ImGui::GetIO().MouseWheel; + if (wheel != 0) { + bool shift = ImGui::GetIO().KeyShift; + bool alt = ImGui::GetIO().KeyAlt; + if (shift || alt) { + float pan_speed = 40.0f; + if (shift) canvas_offset_.x += wheel * pan_speed; + if (alt) canvas_offset_.y += wheel * pan_speed; + } else { + float old_zoom = canvas_zoom_; + canvas_zoom_ *= (wheel > 0) ? 1.1f : 0.9f; + canvas_zoom_ = std::clamp(canvas_zoom_, 0.1f, 10.0f); + ImVec2 mouse = ImGui::GetIO().MousePos; + ImVec2 mouse_rel = v2sub(v2sub(mouse, canvas_p0), canvas_offset_); + ImVec2 mouse_canvas = v2mul(mouse_rel, 1.0f / old_zoom); + canvas_offset_ = v2sub(v2sub(mouse, canvas_p0), v2mul(mouse_canvas, canvas_zoom_)); + } + } + } +} diff --git a/src/attoflow/visual_editor.h b/src/attoflow/visual_editor.h new file mode 100644 index 0000000..d462f15 --- /dev/null +++ b/src/attoflow/visual_editor.h @@ -0,0 +1,71 @@ +#pragma once +#include "atto_editor_shared_state.h" +#include "node_renderer.h" +#include "imgui.h" +#include +#include + +// Reusable 2D canvas interaction layer. +// Provides pan/zoom/select/drag; subclass provides content drawing and hit-testing. +class VisualEditor { +public: + VisualEditor(const std::shared_ptr& shared) : shared_(shared) {} + virtual ~VisualEditor() = default; + + struct CanvasFrame { + ImDrawList* dl; + ImVec2 canvas_p0; + ImVec2 canvas_sz; + ImVec2 canvas_origin; + bool hovered; + }; + + // Call from draw(). Sets up canvas, calls draw_content, handles all interaction. + void draw_canvas(const char* id); + + // Canvas state + ImVec2 canvas_offset() const { return canvas_offset_; } + float canvas_zoom() const { return canvas_zoom_; } + + const HoverItem& hover_item() const { return hover_item_; } + +protected: + ImVec2 canvas_offset_ = {0, 0}; + float canvas_zoom_ = 1.0f; + bool draw_tooltips_ = true; + HoverItem hover_item_; + std::shared_ptr shared_; + + // Interaction state + bool dragging_started_ = false; + bool drag_was_overlapping_ = false; + bool selection_rect_active_ = false; + ImVec2 selection_rect_start_ = {0, 0}; + + // ─── Subclass hooks ─── + + // Draw all content (nodes, wires) inside the clipped canvas + virtual void draw_content(const CanvasFrame& frame) = 0; + + // Find what's under the mouse + virtual HoverItem do_detect_hover(ImVec2 mouse, ImVec2 canvas_origin) = 0; + + // Draw hover highlights and tooltips + virtual void do_draw_hover_effects(ImDrawList* dl, ImVec2 canvas_origin, const HoverItem& hover) = 0; + + // Extract draggable node from hover item (nullptr if not a node) + virtual FlowNodeBuilderPtr hover_to_node(const HoverItem& item) = 0; + + // Test if moving sel to (nx, ny) would overlap non-selected nodes + virtual bool test_drag_overlap(const FlowNodeBuilderPtr& sel, float nx, float ny) = 0; + + // Get all nodes for box-select testing (canvas-space, unzoomed) + struct BoxTestNode { + FlowNodeBuilderPtr node; + float x, y, w, h; + }; + virtual std::vector get_box_test_nodes() = 0; + + // Called when dragging moves selected nodes (subclass can mark wires dirty etc.) + virtual void on_nodes_moved() {} +}; diff --git a/src/attoflow/window.cpp b/src/attoflow/window.cpp index 5e61800..2e612ac 100644 --- a/src/attoflow/window.cpp +++ b/src/attoflow/window.cpp @@ -1,4 +1,5 @@ #include "window.h" +#include "atto/graph_builder.h" #include "atto/args.h" #include "atto/serial.h" #include @@ -32,7 +33,10 @@ bool FlowEditorWindow::init(const std::string& project_dir) { if (tabs_.empty()) { TabState tab; - tab.pane = std::make_shared(); + tab.tab_name = "untitled"; + tab.gb = std::make_shared(); + tab.shared = std::make_shared(); + tab.pane = make_editor2(tab.gb, tab.shared); tabs_.push_back(std::move(tab)); } @@ -57,25 +61,33 @@ void FlowEditorWindow::open_tab(const std::string& file_path) { // Check if already open for (int i = 0; i < (int)tabs_.size(); i++) { - if (tabs_[i].pane && tabs_[i].file_path() == abs_path) { + if (tabs_[i].file_path == abs_path) { active_tab_ = i; return; } } - // Try Editor2Pane first - auto editor2 = std::make_shared(); - std::shared_ptr pane; + TabState tab; + tab.file_path = abs_path; + tab.tab_name = fs::path(file_path).stem().string(); + // Parse file into GraphBuilder if (fs::exists(abs_path)) { - if (!editor2->load(abs_path)) { - throw std::invalid_argument("Cannot load " + abs_path); + std::ifstream f(abs_path); + if (f.is_open()) { + auto result = Deserializer::parse_atto(f); + if (auto* gb = std::get_if>(&result)) { + tab.gb = *gb; + } else { + auto* err = std::get_if(&result); + fprintf(stderr, "Window: %s\n", err ? err->c_str() : "unknown error"); + } } } - pane = editor2; + if (!tab.gb) tab.gb = std::make_shared(); + tab.shared = std::make_shared(); + tab.pane = make_editor2(tab.gb, tab.shared); - TabState tab; - tab.pane = pane; tabs_.push_back(std::move(tab)); active_tab_ = (int)tabs_.size() - 1; } @@ -96,7 +108,10 @@ void FlowEditorWindow::close_tab(int idx) { active_tab_ = std::max(0, (int)tabs_.size() - 1); if (tabs_.empty()) { TabState tab; - tab.pane = std::make_shared(); + tab.tab_name = "untitled"; + tab.gb = std::make_shared(); + tab.shared = std::make_shared(); + tab.pane = make_editor2(tab.gb, tab.shared); tabs_.push_back(std::move(tab)); } } @@ -148,7 +163,7 @@ void FlowEditorWindow::draw() { namespace fs = std::filesystem; std::string stem = fs::path(fname).stem().string(); bool is_active = (active_tab_ < (int)tabs_.size() && - tabs_[active_tab_].tab_name() == stem); + tabs_[active_tab_].tab_name == stem); if (is_active) ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(100, 200, 255, 255)); if (ImGui::Selectable(stem.c_str(), is_active)) { std::string full_path = (fs::path(project_dir_) / fname).string(); @@ -174,8 +189,7 @@ void FlowEditorWindow::draw() { // --- Tab bar --- if (ImGui::BeginTabBar("##atto_tabs")) { for (int i = 0; i < (int)tabs_.size(); i++) { - std::string label = tabs_[i].tab_name(); - if (tabs_[i].is_dirty()) label += "*"; + std::string label = tabs_[i].label(); label += "###tab" + std::to_string(i); bool open = true; ImGuiTabItemFlags flags = (i == active_tab_) ? ImGuiTabItemFlags_SetSelected : 0; @@ -419,8 +433,8 @@ void FlowEditorWindow::run_program(bool release) { } #endif - std::string active_path = active().pane ? active().file_path() : ""; - if (active_path.empty()) return; + if (active().file_path.empty()) return; + std::string active_path = active().file_path; namespace fs = std::filesystem; From 722a5919bf1f096565b0a6858d12cf542f7e328f Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Thu, 26 Mar 2026 23:46:21 +0200 Subject: [PATCH 82/86] move editor1 to legacy folder --- src/{attoflow => legacy}/editor1.cpp | 0 src/{attoflow => legacy}/editor1.h | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/{attoflow => legacy}/editor1.cpp (100%) rename src/{attoflow => legacy}/editor1.h (100%) diff --git a/src/attoflow/editor1.cpp b/src/legacy/editor1.cpp similarity index 100% rename from src/attoflow/editor1.cpp rename to src/legacy/editor1.cpp diff --git a/src/attoflow/editor1.h b/src/legacy/editor1.h similarity index 100% rename from src/attoflow/editor1.h rename to src/legacy/editor1.h From 5838b346a407535a07e9a636ac6246bd585e1daf Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Fri, 27 Mar 2026 00:54:13 +0200 Subject: [PATCH 83/86] nets_editor concept --- CMakeLists.txt | 1 + src/attoflow/editor2.cpp | 16 +- src/attoflow/nets_editor.cpp | 463 +++++++++++++++++++++++++++++++++ src/attoflow/nets_editor.h | 78 ++++++ src/attoflow/node_renderer.cpp | 23 ++ src/attoflow/node_renderer.h | 6 + src/attoflow/window.cpp | 51 ++-- src/attoflow/window.h | 2 + 8 files changed, 599 insertions(+), 41 deletions(-) create mode 100644 src/attoflow/nets_editor.cpp create mode 100644 src/attoflow/nets_editor.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a41e78..997b720 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,7 @@ if(ATTOLANG_BUILD_EDITOR) src/attoflow/main.cpp src/attoflow/window.cpp src/attoflow/editor2.cpp + src/attoflow/nets_editor.cpp src/attoflow/visual_editor.cpp src/attoflow/node_renderer.cpp src/attoflow/tooltip_renderer.cpp diff --git a/src/attoflow/editor2.cpp b/src/attoflow/editor2.cpp index 896dea9..cf47b9b 100644 --- a/src/attoflow/editor2.cpp +++ b/src/attoflow/editor2.cpp @@ -194,21 +194,7 @@ void Editor2Pane::draw_content(const CanvasFrame& frame) { ned->rebuild(frame.canvas_origin, canvas_zoom_); auto& node = ned->node; - NodeRenderState state; - state.selected = shared_ && shared_->selected_nodes.count(node) > 0; - state.node_hovered = false; - if (auto* ep = std::get_if(&hover_item_)) - state.node_hovered = (*ep == node); - state.pin_hovered_on_this = false; - if (auto* pin = std::get_if(&hover_item_)) - state.pin_hovered_on_this = ((*pin)->node() == node); - else if (auto* add = std::get_if(&hover_item_)) - state.pin_hovered_on_this = (add->node == node); - state.hovered_pin = nullptr; - if (auto* pp = std::get_if(&hover_item_)) - state.hovered_pin = *pp; - state.add_pin_hover = std::get_if(&hover_item_); - + auto state = build_render_state(node, hover_item_, shared_.get()); auto* nt = find_node_type2(node->type_id); render_node(frame.dl, node, nt, ned->layout, ned->vpm, ned->display_text, state, canvas_zoom_, draw_tooltips_); diff --git a/src/attoflow/nets_editor.cpp b/src/attoflow/nets_editor.cpp new file mode 100644 index 0000000..35e7578 --- /dev/null +++ b/src/attoflow/nets_editor.cpp @@ -0,0 +1,463 @@ +#include "nets_editor.h" +#include "tooltip_renderer.h" +#include "atto/graph_builder.h" +#include "atto/node_types2.h" +#include "imgui.h" +#include +#include + +// ─── Factory ─── + +NetsEditor::NetsEditor(const std::shared_ptr& gb, + const std::shared_ptr& shared) + : VisualEditor(shared), gb_(gb) { +} + +std::shared_ptr make_nets_editor( + const std::shared_ptr& gb, + const std::shared_ptr& shared) { + return std::make_shared(gb, shared); +} + +// ─── Layout constants ─── + +static constexpr float ROW_HEIGHT_MULT = 6.0f; // rows are 6x node height apart +static constexpr float LEFT_MARGIN = 20.0f; +static constexpr float NODE_GAP = 30.0f; +static constexpr float LABEL_GAP = 60.0f; // gap between src node and label area +static constexpr float FADE_STUB_LENGTH_MULT = 2.0f; // 2x node height + +// ─── Helpers ─── + +// Find which visual output pin of src_node produces this net entry +static int find_source_output_pin(const FlowNodeBuilderPtr& src_node, const BuilderEntryPtr& net_entry) { + for (int k = 0; k < (int)src_node->outputs.size(); k++) { + auto out_net = src_node->outputs[k]->as_net(); + if (out_net && out_net->second() == net_entry) return k; + } + int base = (int)src_node->outputs.size(); + for (int k = 0; k < (int)src_node->outputs_va_args.size(); k++) { + auto out_net = src_node->outputs_va_args[k]->as_net(); + if (out_net && out_net->second() == net_entry) return base + k; + } + return 0; +} + +// Find which visual input pin of dst_node receives this net entry, using VisualPinMap +static int find_dest_input_pin(const VisualPinMap& vpm, const BuilderEntryPtr& net_entry) { + for (int i = 0; i < (int)vpm.inputs.size(); i++) { + auto& pin = vpm.inputs[i]; + if (!pin.arg) continue; + auto an = pin.arg->as_net(); + if (an && an->second() == net_entry) return i; + } + return 0; +} + +// ─── rebuild_layout ─── + +void NetsEditor::rebuild_layout(ImVec2 canvas_origin) { + rows_.clear(); + all_wires_.clear(); + rendered_nodes_.clear(); + + float zoom = canvas_zoom_; + float row_h = S.node_height * ROW_HEIGHT_MULT * zoom; + float node_h = S.node_height * zoom; + + // Collect all non-sentinel nets, sorted alphabetically + std::vector> nets; + for (auto& [id, entry] : gb_->entries) { + auto net = entry->as_net(); + if (!net || net->is_the_unconnected()) continue; + nets.push_back({id, net}); + } + std::sort(nets.begin(), nets.end(), [](auto& a, auto& b) { return a.first < b.first; }); + + // Layout: net label on baseline, nodes above/below with wires curving to baseline + // + // ┌───────────┐ ┌──────────┐ ┌──────────┐ + // │ src_node │ │ dst_a │ │ dst_b │ + // └─────┬─────┘ └─────┬────┘ └────┬─────┘ + // │ (curve down) │ │ + // ──────┴─────── net_label ───────────────┴─────────────┴────── + // (baseline) + + // Start with enough offset so the first row's source node (above baseline) is visible + float first_row_offset = node_h * ROW_HEIGHT_MULT; + + int row_idx = 0; + for (auto& [net_id, net] : nets) { + auto src_ptr = net->source().lock(); + auto src_node = src_ptr ? src_ptr->as_node() : nullptr; + if (!src_node) { row_idx++; continue; } + + NetRow row; + row.net = net; + row.net_id = net_id; + row.src_node = src_node; + row.row_y = first_row_offset + row_idx * row_h; + + float baseline_y = canvas_origin.y + row.row_y; + + // Source node + auto* src_nt = find_node_type2(src_node->type_id); + row.src_vpm = VisualPinMap::build(src_node, src_nt); + row.src_display = src_nt ? src_nt->name : "?"; + std::string args = src_node->args_str(); + if (!args.empty()) row.src_display += " " + args; + + row.src_output_pin = find_source_output_pin(src_node, net); + row.src_is_bang = src_nt && row.src_output_pin < src_nt->num_outputs && + src_nt->output_ports && + src_nt->output_ports[row.src_output_pin].kind == PortKind2::BangNext; + + // Source node placement: put node on OPPOSITE side of baseline from its pin. + // Output pins (bottom of node) → node ABOVE baseline, pin near baseline, wire curves down. + // Side-bang (right of node) → node above baseline (side-bang is mid-height). + row.src_layout = compute_node_layout(src_node, row.src_vpm, {0,0}, zoom); + float src_x = canvas_origin.x + LEFT_MARGIN * zoom; + float gap = node_h * 0.3f; + + bool is_side_bang = row.src_is_bang && row.src_vpm.has_side_bang; + // Output pins are at the bottom → node above baseline + row.src_layout.pos = {src_x, baseline_y - row.src_layout.height - gap * 3}; + + rendered_nodes_.push_back({src_node, row.src_layout, row.src_vpm, row.src_display}); + + // Get source pin position + ImVec2 src_pin_pos; + if (is_side_bang) { + src_pin_pos = row.src_layout.side_bang_pos(); + } else { + int visual_pin = row.src_output_pin; + if (row.src_vpm.is_flow) visual_pin = std::max(0, visual_pin - 1); + src_pin_pos = row.src_layout.output_pin_pos(visual_pin); + } + ImVec2 src_baseline = {src_pin_pos.x, baseline_y}; + + // Collect destinations sorted alphabetically + net->compact(); + std::vector> dest_list; + for (auto& dw : net->destinations()) { + auto dp = dw.lock(); + auto dn = dp ? dp->as_node() : nullptr; + if (dn) dest_list.push_back({dn->id(), dn}); + } + std::sort(dest_list.begin(), dest_list.end(), [](auto& a, auto& b) { return a.first < b.first; }); + + // Net label position: between source and first destination + float stub_len = node_h * FADE_STUB_LENGTH_MULT; + float src_extra = row.src_vpm.has_side_bang ? stub_len + S.pin_radius * zoom : 0.0f; + float label_x = src_x + row.src_layout.width + src_extra + LABEL_GAP * 0.5f * zoom; + float dest_start_x = src_x + row.src_layout.width + src_extra + LABEL_GAP * zoom; + + // Wire: source pin → baseline (pin is above baseline, curves down) + { + float curve_dy = std::abs(src_pin_pos.y - baseline_y) * 0.5f; + ImVec2 cp1 = {src_pin_pos.x, src_pin_pos.y + curve_dy}; + ImVec2 cp2 = {src_baseline.x, src_baseline.y - curve_dy}; + all_wires_.push_back({net, src_pin_pos, cp1, cp2, src_baseline, + src_node->id(), "", net_id}); + } + + // Deduplicate destinations: group by node ID, collect all input pins + // A node may appear multiple times if it has multiple pins connected to this net + struct DestGroup { + FlowNodeBuilderPtr node; + NodeId id; + std::vector input_pins; // all visual input pin indices connected to this net + }; + std::map dest_groups; + for (auto& [did, dnode] : dest_list) { + auto& grp = dest_groups[did]; + if (!grp.node) { + grp.node = dnode; + grp.id = did; + } + auto dvpm = VisualPinMap::build(dnode, find_node_type2(dnode->type_id)); + // Find ALL input pins connected to this net (not just the first) + for (int i = 0; i < (int)dvpm.inputs.size(); i++) { + auto& pin = dvpm.inputs[i]; + if (!pin.arg) continue; + auto an = pin.arg->as_net(); + if (an && an->second() == net) grp.input_pins.push_back(i); + } + } + + // Sort groups alphabetically and lay out + std::vector sorted_groups; + for (auto& [id, grp] : dest_groups) sorted_groups.push_back(&grp); + std::sort(sorted_groups.begin(), sorted_groups.end(), + [](auto* a, auto* b) { return a->id < b->id; }); + + float dest_x = dest_start_x; + for (auto* grp : sorted_groups) { + auto* dst_nt = find_node_type2(grp->node->type_id); + NetRow::Dest dest; + dest.node = grp->node; + dest.vpm = VisualPinMap::build(grp->node, dst_nt); + dest.display = dst_nt ? dst_nt->name : "?"; + std::string dargs = grp->node->args_str(); + if (!dargs.empty()) dest.display += " " + dargs; + dest.input_pin = grp->input_pins.empty() ? 0 : grp->input_pins[0]; + dest.is_bang = dest.input_pin < (int)dest.vpm.inputs.size() && + dest.vpm.inputs[dest.input_pin].port_kind == PortKind2::BangTrigger; + + // Node below baseline, more gap + dest.layout = compute_node_layout(grp->node, dest.vpm, {0,0}, zoom); + dest.layout.pos = {dest_x, baseline_y + gap * 3}; + + rendered_nodes_.push_back({grp->node, dest.layout, dest.vpm, dest.display}); + + // Wire for EACH connected pin on this node + for (int pin_idx : grp->input_pins) { + ImVec2 dst_pin_pos = dest.layout.input_pin_pos(pin_idx); + ImVec2 dst_baseline = {dst_pin_pos.x, baseline_y}; + float curve_dy = std::abs(dst_pin_pos.y - baseline_y) * 0.5f; + ImVec2 cp1 = {dst_baseline.x, dst_baseline.y + curve_dy}; + ImVec2 cp2 = {dst_pin_pos.x, dst_pin_pos.y - curve_dy}; + all_wires_.push_back({net, dst_baseline, cp1, cp2, dst_pin_pos, + "", grp->id, net_id}); + } + + // Extra gap if node has side-bang stub (extends past right edge) + float extra = dest.vpm.has_side_bang ? stub_len + S.pin_radius * zoom : 0.0f; + dest_x += dest.layout.width + extra + NODE_GAP * zoom; + row.dests.push_back(std::move(dest)); + } + + row.label_x = label_x; + + // Add short stub wires for non-primary connections on all rendered nodes + auto is_unconnected = [](const BuilderEntryPtr& e) { + if (!e) return true; + auto n = e->as_net(); + return n && n->is_the_unconnected(); + }; + + auto add_stubs = [&](const NodeLayout& layout, const VisualPinMap& vpm, + const BuilderEntryPtr& primary_net) { + float stub_len = node_h * FADE_STUB_LENGTH_MULT; + // Output pin stubs (curve downward from pin) + for (int i = 0; i < (int)vpm.outputs.size(); i++) { + auto& pin = vpm.outputs[i]; + if (!pin.arg) continue; + auto an = pin.arg->as_net(); + if (!an) continue; + auto entry = an->second(); + if (!entry || entry == primary_net || is_unconnected(entry)) continue; + ImVec2 pp = layout.output_pin_pos(i); + ImVec2 end = {pp.x, pp.y + stub_len}; + all_wires_.push_back({entry, pp, {pp.x, pp.y + stub_len * 0.3f}, + {pp.x, pp.y + stub_len * 0.7f}, end, + "", "", an->first()}); + } + // Input pin stubs (curve upward from pin) + for (int i = 0; i < (int)vpm.inputs.size(); i++) { + auto& pin = vpm.inputs[i]; + if (!pin.arg || pin.kind == VisualPinKind::AddDiamond || pin.kind == VisualPinKind::AbsentOptional) continue; + auto an = pin.arg->as_net(); + if (!an) continue; + auto entry = an->second(); + if (!entry || entry == primary_net || is_unconnected(entry)) continue; + ImVec2 pp = layout.input_pin_pos(i); + ImVec2 end = {pp.x, pp.y - stub_len}; + all_wires_.push_back({entry, pp, {pp.x, pp.y - stub_len * 0.3f}, + {pp.x, pp.y - stub_len * 0.7f}, end, + "", "", an->first()}); + } + // Side-bang stub + if (vpm.has_side_bang && vpm.side_bang_arg) { + auto an = vpm.side_bang_arg->as_net(); + if (an) { + auto entry = an->second(); + if (entry && entry != primary_net && !is_unconnected(entry)) { + ImVec2 pp = layout.side_bang_pos(); + ImVec2 end = {pp.x + stub_len, pp.y}; + all_wires_.push_back({entry, pp, {pp.x + stub_len * 0.3f, pp.y}, + {pp.x + stub_len * 0.7f, pp.y}, end, + "", "", an->first()}); + } + } + } + }; + + add_stubs(row.src_layout, row.src_vpm, net); + for (auto& dest : row.dests) + add_stubs(dest.layout, dest.vpm, net); + + // Baseline as two flat wire segments (left of label, right of label) + // so they get automatic hit-testing and highlighting + float bl_left = src_baseline.x; + float bl_right = dest_x > dest_start_x ? dest_x : dest_start_x; + // Estimate label width for the gap + ImVec2 label_sz = ImGui::CalcTextSize(net_id.c_str()); + float label_w = label_sz.x * canvas_zoom_ * 0.8f / ImGui::GetFontSize() * ImGui::GetFontSize(); + // Simpler: just use a fixed estimate + float label_half = (label_sz.x * zoom * 0.8f + 6.0f) * 0.5f; + float label_cx = label_x + label_half; + + // Left segment: source baseline → just before label + if (label_x - 3.0f * zoom > bl_left) { + ImVec2 l0 = {bl_left, baseline_y}; + ImVec2 l1 = {label_x - 3.0f * zoom, baseline_y}; + all_wires_.push_back({net, l0, l0, l1, l1, + src_node->id(), "", net_id}); + } + // Right segment: just after label → rightmost dest + if (bl_right > label_x + label_half * 2 + 3.0f * zoom) { + ImVec2 r0 = {label_x + label_half * 2 + 3.0f * zoom, baseline_y}; + ImVec2 r1 = {bl_right, baseline_y}; + all_wires_.push_back({net, r0, r0, r1, r1, + "", "", net_id}); + } + + rows_.push_back(std::move(row)); + row_idx++; + } +} + +// ─── Draw ─── + +void NetsEditor::draw() { + if (!gb_) { + ImGui::TextDisabled("No graph loaded"); + return; + } + draw_canvas("##canvas_nets"); +} + +// ─── draw_content ─── + +void NetsEditor::draw_content(const CanvasFrame& frame) { + rebuild_layout(frame.canvas_origin); + + float zoom = canvas_zoom_; + + float row_h = S.node_height * ROW_HEIGHT_MULT * zoom; + + // Track which nodes already had tooltips to avoid duplicates + std::set tooltipped_nodes; + + for (int ri = 0; ri < (int)rows_.size(); ri++) { + auto& row = rows_[ri]; + float baseline_y = frame.canvas_origin.y + row.row_y; + + // Draw separator between rows + if (ri > 0) { + float sep_y = baseline_y - row_h * 0.5f; + float sep_left = frame.canvas_origin.x; + float sep_right = sep_left + frame.canvas_sz.x / zoom * 2.0f; // wide enough + frame.dl->AddLine({sep_left, sep_y}, {sep_right, sep_y}, + IM_COL32(60, 60, 80, 120), 1.0f); + } + + // Render source node (only show tooltip on first occurrence) + auto* src_nt = find_node_type2(row.src_node->type_id); + auto src_state = build_render_state(row.src_node, hover_item_, shared_.get()); + bool src_tt = draw_tooltips_ && tooltipped_nodes.insert(row.src_node).second; + render_node(frame.dl, row.src_node, src_nt, row.src_layout, row.src_vpm, + row.src_display, src_state, zoom, src_tt); + + // Render destination nodes + for (auto& dest : row.dests) { + auto* dst_nt = find_node_type2(dest.node->type_id); + auto dst_state = build_render_state(dest.node, hover_item_, shared_.get()); + bool dst_tt = draw_tooltips_ && tooltipped_nodes.insert(dest.node).second; + render_node(frame.dl, dest.node, dst_nt, dest.layout, dest.vpm, + dest.display, dst_state, zoom, dst_tt); + } + } + + // Draw all wires (baseline segments are included as WireInfo) + for (auto& w : all_wires_) { + render_wire(frame.dl, w, zoom); + } + + // Draw net labels ON TOP of wires so highlights don't cover them + float font_size = ImGui::GetFontSize() * zoom * 0.8f; + if (font_size > 5.0f) { + for (auto& row : rows_) { + float baseline_y = frame.canvas_origin.y + row.row_y; + ImVec2 text_sz = ImGui::CalcTextSize(row.net_id.c_str()); + float tw = text_sz.x * (font_size / ImGui::GetFontSize()); + float th = text_sz.y * (font_size / ImGui::GetFontSize()); + frame.dl->AddRectFilled({row.label_x - 3, baseline_y - th * 0.5f - 1}, + {row.label_x + tw + 3, baseline_y + th * 0.5f + 1}, + S.col_label_bg, S.node_rounding); + frame.dl->AddText(nullptr, font_size, {row.label_x, baseline_y - th * 0.5f}, + S.col_label_text, row.net_id.c_str()); + } + } +} + +// ─── Hover detection ─── + +HoverItem NetsEditor::do_detect_hover(ImVec2 mouse, ImVec2 canvas_origin) { + // Build hit targets from all rendered node instances + std::vector targets; + targets.reserve(rendered_nodes_.size()); + for (auto& rn : rendered_nodes_) { + auto* nt = find_node_type2(rn.node->type_id); + if (!nt) continue; + targets.push_back({rn.node, nt, &rn.layout, &rn.vpm}); + } + + auto wire_hit = hit_test_wires(mouse, all_wires_, canvas_zoom_); + auto node_hit = hit_test_node_bodies(mouse, targets, canvas_zoom_); + auto pin_hit = hit_test_pins(mouse, targets, canvas_zoom_); + + HitResult best = wire_hit; + if (node_hit.distance < best.distance) best = node_hit; + if (pin_hit.distance < best.distance) best = pin_hit; + + return best.item; +} + +void NetsEditor::do_draw_hover_effects(ImDrawList* dl, ImVec2 canvas_origin, const HoverItem& hover) { + if (std::holds_alternative(hover)) return; + + BuilderEntryPtr hover_entry = nullptr; + if (auto* ep = std::get_if(&hover)) { + hover_entry = *ep; + } + + // Highlight all wire segments sharing the hovered entry (nets and lambda nodes) + if (hover_entry) { + for (auto& w : all_wires_) { + if (w.entry() == hover_entry) + render_wire_highlight(dl, w, canvas_zoom_); + } + if (draw_tooltips_) { + for (auto& w : all_wires_) { + if (w.entry() == hover_entry) { + tooltip_wire(w); + break; + } + } + } + } +} + +FlowNodeBuilderPtr NetsEditor::hover_to_node(const HoverItem& item) { + if (auto* ep = std::get_if(&item)) { + if (*ep) return (*ep)->as_node(); + } else if (auto* pin = std::get_if(&item)) { + return (*pin)->node(); + } + return nullptr; +} + +bool NetsEditor::test_drag_overlap(const FlowNodeBuilderPtr&, float, float) { + return true; // Dragging disabled — positions are computed +} + +std::vector NetsEditor::get_box_test_nodes() { + std::vector result; + for (auto& rn : rendered_nodes_) { + result.push_back({rn.node, rn.layout.pos.x, rn.layout.pos.y, + rn.layout.width, rn.layout.height}); + } + return result; +} diff --git a/src/attoflow/nets_editor.h b/src/attoflow/nets_editor.h new file mode 100644 index 0000000..72a6d0c --- /dev/null +++ b/src/attoflow/nets_editor.h @@ -0,0 +1,78 @@ +#pragma once +#include "editor_pane.h" +#include "visual_editor.h" +#include +#include +#include + +class NetsEditor : public IEditorPane, public VisualEditor { +public: + NetsEditor(const std::shared_ptr& gb, + const std::shared_ptr& shared); + + // IEditorPane + void draw() override; + const char* type_name() const override { return "nets"; } + std::shared_ptr get_graph_builder() const override { return gb_; } + +protected: + // VisualEditor hooks + void draw_content(const CanvasFrame& frame) override; + HoverItem do_detect_hover(ImVec2 mouse, ImVec2 canvas_origin) override; + void do_draw_hover_effects(ImDrawList* dl, ImVec2 canvas_origin, const HoverItem& hover) override; + FlowNodeBuilderPtr hover_to_node(const HoverItem& item) override; + bool test_drag_overlap(const FlowNodeBuilderPtr& sel, float nx, float ny) override; + std::vector get_box_test_nodes() override; + +private: + std::shared_ptr gb_; + + // Per-row layout for each net + struct NetRow { + NetBuilderPtr net; + NodeId net_id; + + // Source node + FlowNodeBuilderPtr src_node; + NodeLayout src_layout; + VisualPinMap src_vpm; + std::string src_display; + int src_output_pin; // output pin index connected to this net + bool src_is_bang; // source is a bang (node below line) + + // Destination nodes (sorted alphabetically) + struct Dest { + FlowNodeBuilderPtr node; + NodeLayout layout; + VisualPinMap vpm; + std::string display; + int input_pin; // visual input pin index receiving this net + bool is_bang; // destination is a bang trigger + }; + std::vector dests; + + float row_y; // canvas-space Y baseline + float label_x; // screen-space X for net label + }; + + std::vector rows_; + + // All wires for hit-testing + std::vector all_wires_; + + // All rendered node instances for hit-testing + struct RenderedNode { + FlowNodeBuilderPtr node; + NodeLayout layout; + VisualPinMap vpm; + std::string display; + }; + std::vector rendered_nodes_; + + void rebuild_layout(ImVec2 canvas_origin); +}; + +// Factory +std::shared_ptr make_nets_editor( + const std::shared_ptr& gb, + const std::shared_ptr& shared); diff --git a/src/attoflow/node_renderer.cpp b/src/attoflow/node_renderer.cpp index 6361798..31148d7 100644 --- a/src/attoflow/node_renderer.cpp +++ b/src/attoflow/node_renderer.cpp @@ -1,8 +1,31 @@ #include "node_renderer.h" +#include "atto_editor_shared_state.h" #include "tooltip_renderer.h" #include #include +// ─── build_render_state ─── + +NodeRenderState build_render_state(const FlowNodeBuilderPtr& node, + const HoverItem& hover_item, + const AttoEditorSharedState* shared) { + NodeRenderState state; + state.selected = shared && shared->selected_nodes.count(node) > 0; + state.node_hovered = false; + if (auto* ep = std::get_if(&hover_item)) + state.node_hovered = (*ep == node); + state.pin_hovered_on_this = false; + if (auto* pin = std::get_if(&hover_item)) + state.pin_hovered_on_this = ((*pin)->node() == node); + else if (auto* add = std::get_if(&hover_item)) + state.pin_hovered_on_this = (add->node == node); + state.hovered_pin = nullptr; + if (auto* pp = std::get_if(&hover_item)) + state.hovered_pin = *pp; + state.add_pin_hover = std::get_if(&hover_item); + return state; +} + // ─── Geometry helpers ─── float point_to_bezier_dist(ImVec2 p, ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3) { diff --git a/src/attoflow/node_renderer.h b/src/attoflow/node_renderer.h index 18cc91d..08346ed 100644 --- a/src/attoflow/node_renderer.h +++ b/src/attoflow/node_renderer.h @@ -96,6 +96,12 @@ struct NodeRenderState { const AddPinHover* add_pin_hover; // null if no +diamond hovered }; +struct AttoEditorSharedState; + +NodeRenderState build_render_state(const FlowNodeBuilderPtr& node, + const HoverItem& hover_item, + const AttoEditorSharedState* shared); + // ─── Hit-testing ─── struct HitResult { diff --git a/src/attoflow/window.cpp b/src/attoflow/window.cpp index 2e612ac..db78566 100644 --- a/src/attoflow/window.cpp +++ b/src/attoflow/window.cpp @@ -31,15 +31,6 @@ bool FlowEditorWindow::init(const std::string& project_dir) { } } - if (tabs_.empty()) { - TabState tab; - tab.tab_name = "untitled"; - tab.gb = std::make_shared(); - tab.shared = std::make_shared(); - tab.pane = make_editor2(tab.gb, tab.shared); - tabs_.push_back(std::move(tab)); - } - return true; } @@ -59,10 +50,11 @@ void FlowEditorWindow::open_tab(const std::string& file_path) { namespace fs = std::filesystem; std::string abs_path = fs::absolute(file_path).string(); - // Check if already open + // Check if already open (match on file_path + graph editor type) for (int i = 0; i < (int)tabs_.size(); i++) { - if (tabs_[i].file_path == abs_path) { - active_tab_ = i; + if (tabs_[i].file_path == abs_path && tabs_[i].pane && + std::string(tabs_[i].pane->type_name()) == "graph") { + pending_tab_select_ = i; return; } } @@ -88,8 +80,17 @@ void FlowEditorWindow::open_tab(const std::string& file_path) { tab.shared = std::make_shared(); tab.pane = make_editor2(tab.gb, tab.shared); + // Create nets editor tab sharing the same graph + state + TabState nets_tab; + nets_tab.file_path = abs_path; + nets_tab.tab_name = tab.tab_name; + nets_tab.gb = tab.gb; + nets_tab.shared = tab.shared; + nets_tab.pane = make_nets_editor(nets_tab.gb, nets_tab.shared); + tabs_.push_back(std::move(tab)); - active_tab_ = (int)tabs_.size() - 1; + tabs_.push_back(std::move(nets_tab)); + pending_tab_select_ = (int)tabs_.size() - 2; // focus on the graph editor tab } void FlowEditorWindow::close_tab(int idx) { @@ -106,14 +107,6 @@ void FlowEditorWindow::close_tab(int idx) { tabs_.erase(tabs_.begin() + idx); if (active_tab_ >= (int)tabs_.size()) active_tab_ = std::max(0, (int)tabs_.size() - 1); - if (tabs_.empty()) { - TabState tab; - tab.tab_name = "untitled"; - tab.gb = std::make_shared(); - tab.shared = std::make_shared(); - tab.pane = make_editor2(tab.gb, tab.shared); - tabs_.push_back(std::move(tab)); - } } void FlowEditorWindow::shutdown() { @@ -188,15 +181,15 @@ void FlowEditorWindow::draw() { // --- Tab bar --- if (ImGui::BeginTabBar("##atto_tabs")) { + int pending_select = pending_tab_select_; + pending_tab_select_ = -1; for (int i = 0; i < (int)tabs_.size(); i++) { std::string label = tabs_[i].label(); label += "###tab" + std::to_string(i); bool open = true; - ImGuiTabItemFlags flags = (i == active_tab_) ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags flags = (i == pending_select) ? ImGuiTabItemFlags_SetSelected : 0; if (ImGui::BeginTabItem(label.c_str(), &open, flags)) { - if (active_tab_ != i) { - active_tab_ = i; - } + active_tab_ = i; ImGui::EndTabItem(); } if (!open) { @@ -215,8 +208,14 @@ void FlowEditorWindow::draw() { // --- Canvas --- ImGui::BeginChild("##flow_canvas", {canvas_w, canvas_h}, false, ImGuiWindowFlags_NoScrollbar); - if (active().pane) { + if (!tabs_.empty() && active().pane) { active().pane->draw(); + } else { + ImVec2 sz = ImGui::GetContentRegionAvail(); + const char* msg = "Select a file from the file list to open it."; + ImVec2 text_sz = ImGui::CalcTextSize(msg); + ImGui::SetCursorPos({(sz.x - text_sz.x) * 0.5f, (sz.y - text_sz.y) * 0.5f}); + ImGui::TextDisabled("%s", msg); } ImGui::EndChild(); diff --git a/src/attoflow/window.h b/src/attoflow/window.h index dcf6bf7..e86a69d 100644 --- a/src/attoflow/window.h +++ b/src/attoflow/window.h @@ -2,6 +2,7 @@ #include "sdl_imgui_window.h" #include "tab.h" #include "editor2.h" +#include "nets_editor.h" #include #include #include @@ -41,6 +42,7 @@ class FlowEditorWindow { // Tabs std::vector tabs_; int active_tab_ = 0; + int pending_tab_select_ = -1; // one-shot: set to force tab selection next frame // Panel sizes float side_panel_width_ = 200.0f; From 84dc2daf318296d529898d2b1c629fe2320427ba Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Fri, 27 Mar 2026 01:00:03 +0200 Subject: [PATCH 84/86] Add scroll pan speed to Editor2Style and update canvas panning logic --- src/attoflow/editor_style.cpp | 2 ++ src/attoflow/editor_style.h | 3 +++ src/attoflow/visual_editor.cpp | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/attoflow/editor_style.cpp b/src/attoflow/editor_style.cpp index 8fad4bb..2b3d8f4 100644 --- a/src/attoflow/editor_style.cpp +++ b/src/attoflow/editor_style.cpp @@ -45,6 +45,8 @@ Editor2Style::Editor2Style() // Net label colors , col_label_bg(IM_COL32(30, 30, 40, 200)) , col_label_text(IM_COL32(180, 220, 255, 255)) + // Interaction + , scroll_pan_speed(120.0f) // Tooltip , tooltip_scale(1.0f) { diff --git a/src/attoflow/editor_style.h b/src/attoflow/editor_style.h index 5983592..2eee00e 100644 --- a/src/attoflow/editor_style.h +++ b/src/attoflow/editor_style.h @@ -56,6 +56,9 @@ struct Editor2Style { ImU32 col_label_bg; ImU32 col_label_text; + // Interaction + float scroll_pan_speed; + // Tooltip float tooltip_scale; }; diff --git a/src/attoflow/visual_editor.cpp b/src/attoflow/visual_editor.cpp index 97548ae..5582ef2 100644 --- a/src/attoflow/visual_editor.cpp +++ b/src/attoflow/visual_editor.cpp @@ -137,7 +137,7 @@ void VisualEditor::draw_canvas(const char* id) { bool shift = ImGui::GetIO().KeyShift; bool alt = ImGui::GetIO().KeyAlt; if (shift || alt) { - float pan_speed = 40.0f; + float pan_speed = S.scroll_pan_speed; if (shift) canvas_offset_.x += wheel * pan_speed; if (alt) canvas_offset_.y += wheel * pan_speed; } else { From 2aaa7f46b36e2619008bc42f6bad210365ba63de Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Fri, 27 Mar 2026 01:05:56 +0200 Subject: [PATCH 85/86] fix: shorten node ids --- src/atto/graph_builder.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index 4e460d9..80901fc 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -1442,10 +1442,12 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { for (auto& o : node_p->outputs_va_args) if (auto n = o->as_net()) n->net_id(remap_id(n->first())); } - // Rebuild entries map with new keys + // Rebuild entries map with new keys and update entry IDs std::map new_entries; for (auto& [id, entry] : gb->entries) { - new_entries[remap_id(id)] = std::move(entry); + auto new_id = remap_id(id); + entry->id(new_id); + new_entries[new_id] = std::move(entry); } gb->entries = std::move(new_entries); } From 27617c56155acbea8042e30efa0fe1b41f2dc05f Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Fri, 27 Mar 2026 01:08:42 +0200 Subject: [PATCH 86/86] in atto:0 import remove unconnected nets and replace with $unconnected --- src/atto/graph_builder.cpp | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/atto/graph_builder.cpp b/src/atto/graph_builder.cpp index 80901fc..9de4c41 100644 --- a/src/atto/graph_builder.cpp +++ b/src/atto/graph_builder.cpp @@ -1386,6 +1386,57 @@ Deserializer::ParseAttoResult Deserializer::parse_atto(std::istream& f) { } } + // ─── Remove nets with no destinations → replace with $unconnected ─── + { + auto unconnected = gb->unconnected_net(); + auto unconnected_entry = std::static_pointer_cast(unconnected); + + // Collect nets to remove (have source but no destinations) + std::set dead_nets; + for (auto& [id, entry] : gb->entries) { + auto net = entry->as_net(); + if (!net || net->is_the_unconnected()) continue; + net->compact(); + if (net->destinations().empty()) { + dead_nets.insert(entry); + } + } + + if (!dead_nets.empty()) { + // Replace all ArgNet2 references to dead nets with $unconnected + auto fixup_arg = [&](const FlowArg2Ptr& a) { + auto n = a->as_net(); + if (!n) return; + if (dead_nets.count(n->second())) { + n->entry(unconnected_entry); + n->net_id(unconnected->id()); + } + }; + auto fixup_args = [&](ParsedArgs2* pa) { + if (!pa) return; + for (auto& a : *pa) fixup_arg(a); + }; + + for (auto& [id, entry] : gb->entries) { + auto node = entry->as_node(); + if (!node) continue; + fixup_args(node->parsed_args.get()); + fixup_args(node->parsed_va_args.get()); + for (auto& r : node->remaps) fixup_arg(r); + for (auto& o : node->outputs) fixup_arg(o); + for (auto& o : node->outputs_va_args) fixup_arg(o); + } + + // Remove dead nets from entries + for (auto it = gb->entries.begin(); it != gb->entries.end(); ) { + if (dead_nets.count(it->second)) + it = gb->entries.erase(it); + else + ++it; + } + } + } + // ─── Re-ID: $auto-xxx → $a-N (compact hex IDs) ─── { // Build rename map for $auto- entries