Conversation
… update GraphBuilder to handle parsed arguments
- 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.
- docs/instructions.md: Guide covering build system, architectural layers, instrument anatomy, audio callback patterns, and codebase conventions - docs/names.md: Naming philosophy and guidelines for the project - README.md: Link new docs from the Instructions section
There was a problem hiding this comment.
Pull request overview
This PR modernizes the attoflow editor by replacing the legacy monolithic editor with a layered Editor2 architecture built around GraphBuilder (observer-driven model) and a reusable VisualEditor canvas interaction layer, plus extensive accompanying documentation.
Changes:
- Introduces
GraphBuilder+ editor observer interfaces and migrates the editor UI toFlowEditorWindow+Editor2Pane+VisualEditor. - Adds a dedicated nets view (
NetsEditor) and shared selection state across panes/tabs. - Embeds Liberation Mono for ImGui and adds a large documentation suite describing architecture, style, patterns, and format rules.
Reviewed changes
Copilot reviewed 42 out of 45 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/legacy/editor1.h | Adds legacy Editor1 pane header in legacy/ for compatibility/transition. |
| src/attoflow/window.h | New top-level window class API (tabs, build/run, child process mgmt). |
| src/attoflow/visual_editor.h | New reusable canvas interaction base class (pan/zoom/hover/drag/wire ops). |
| src/attoflow/visual_editor.cpp | Implements canvas drawing + interactions (selection, dragging, wire drag/grab). |
| src/attoflow/tooltip_renderer.h | Declares tooltip render helpers for pins/nodes/wires. |
| src/attoflow/tooltip_renderer.cpp | Implements tooltip content for pins, node body, and wire info. |
| src/attoflow/tab.h | Defines per-tab state to hold GraphBuilder, shared editor state, and pane instance. |
| src/attoflow/sdl_imgui_window.h | Switches to embedded Liberation Mono font and DPI-aware font rebuilding. |
| src/attoflow/node_renderer.h | Declares rendering + hit-testing primitives and hover item types. |
| src/attoflow/nets_editor.h | Adds a nets-focused editor pane built on VisualEditor. |
| src/attoflow/nets_editor.cpp | Implements nets layout/rendering + hover highlighting in a baseline-style view. |
| src/attoflow/main.cpp | Updates entry include to use the new window.h entry point. |
| src/attoflow/fonts/LICENSE-LiberationFonts | Adds font license for Liberation fonts. |
| src/attoflow/editor_style.h | Adds centralized editor style constants (Editor2Style, global S). |
| src/attoflow/editor_style.cpp | Defines default values for Editor2Style and instantiates global S. |
| src/attoflow/editor_pane.h | Introduces IEditorPane interface for pluggable panes per tab. |
| src/attoflow/editor2.h | Declares Editor2 pane, per-item editor impls, and wire connection hooks. |
| src/attoflow/editor2.cpp | Implements Editor2 rendering, hover, wire rebuild, connect/disconnect/delete logic. |
| src/attoflow/editor.h | Removes the legacy monolithic editor window/header. |
| src/attoflow/atto_editor_shared_state.h | Adds shared selection state used across panes. |
| src/atto/node_types.h | Adds NodeTypeID::Error descriptor entry. |
| src/atto/graph_editor_interfaces.h | Adds editor observer interfaces (IGraphEditor, INodeEditor, etc.). |
| src/atto/graph_builder.h | Adds the GraphBuilder model (entries/pins/sentinels, batching, observer hooks). |
| src/atto/args.h | Adds split_args API and clarifies legacy wrapper comment. |
| src/atto/args.cpp | Implements split_args tokenizer/splitting logic for arg strings. |
| scenes/klavier/main.atto | Updates shadow expr nodes to encode return types inline (folding -> nodes). |
| docs/thinking.md | New design/philosophy write-up for major project decisions. |
| docs/style.md | New comprehensive code + visual style guide. |
| docs/patterns.md | New catalog of recurring design patterns used throughout the codebase. |
| docs/names.md | New naming philosophy and naming history document. |
| docs/differential.md | New document on incremental/differential concepts across the system. |
| docs/coding.md | New C++ coding conventions and repository practices guide. |
| docs/changelog.md | New project evolution and milestone changelog. |
| docs/attolang.md | Updates language spec sections (inline args, bang semantics, file format details). |
| docs/architecture.md | New high-level architecture overview and subsystem breakdown. |
| README.md | Adds pointers to the new documentation suite. |
| CMakeLists.txt | Adds graph_builder.cpp to core library and generates embedded font header for editor build. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 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; |
There was a problem hiding this comment.
This helper appears unused in this TU, which can trip -Werror=unused-function builds. Either remove it or use it for destination pin lookup.
| 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; |
There was a problem hiding this comment.
label_w / label_cx are computed but never used, which can trigger unused-variable warnings. Either remove them or use them in the label-gap computation.
| // Remove from net's destinations if this is a dest pin | ||
| auto node = pin->node(); | ||
| auto& dests = net->destinations(); | ||
| dests.erase(std::remove_if(dests.begin(), dests.end(), | ||
| [&](auto& w) { return w.lock() == node; }), dests.end()); |
There was a problem hiding this comment.
This removal always treats the pin as a destination (destinations() entry). If the disconnected pin is an output/source pin, you need to clear net->source() instead; otherwise the net is left with a stale source. Use the pos parameter (or equivalent) to branch.
| // Remove from net's destinations if this is a dest pin | |
| auto node = pin->node(); | |
| auto& dests = net->destinations(); | |
| dests.erase(std::remove_if(dests.begin(), dests.end(), | |
| [&](auto& w) { return w.lock() == node; }), dests.end()); | |
| // Update net endpoints based on pin position | |
| auto node = pin->node(); | |
| if (pos == PortPosition2::Output) { | |
| // Disconnecting a source/output pin: clear the net's source | |
| net->source({}); | |
| } else { | |
| // Disconnecting a destination/input pin: remove from destinations | |
| auto& dests = net->destinations(); | |
| dests.erase(std::remove_if(dests.begin(), dests.end(), | |
| [&](auto& w) { return w.lock() == node; }), dests.end()); | |
| } |
| // Remove this node from the net's destinations | ||
| auto node = pin->node(); | ||
| auto& dests = net->destinations(); | ||
| dests.erase(std::remove_if(dests.begin(), dests.end(), | ||
| [&](auto& w) { return w.lock() == node; }), dests.end()); | ||
|
|
There was a problem hiding this comment.
This code always removes the pin’s node from net->destinations(). Deleting/disconnecting an output/source pin should clear net->source() instead (and keep the net consistent), otherwise the net retains a stale source.
| // Remove this node from the net's destinations | |
| auto node = pin->node(); | |
| auto& dests = net->destinations(); | |
| dests.erase(std::remove_if(dests.begin(), dests.end(), | |
| [&](auto& w) { return w.lock() == node; }), dests.end()); | |
| auto node = pin->node(); | |
| auto& dests = net->destinations(); | |
| auto& src = net->source(); | |
| // If this node is the net's source, clear the source; otherwise, remove it from destinations | |
| if (src.lock() == node) { | |
| src.reset(); | |
| } else { | |
| dests.erase(std::remove_if(dests.begin(), dests.end(), | |
| [&](auto& w) { return w.lock() == node; }), dests.end()); | |
| } |
|
|
||
| // ─── Wire rebuilding ─── | ||
|
|
||
| void Editor2Pane::rebuild_wires(ImVec2 canvas_origin) { |
There was a problem hiding this comment.
rebuild_wires() always clears and rebuilds cached_wires_ even though the class tracks wires_dirty_. Consider early-returning when !wires_dirty_ (and only setting dirty via observer callbacks / on_nodes_moved) to avoid rebuilding wires every frame.
| void Editor2Pane::rebuild_wires(ImVec2 canvas_origin) { | |
| void Editor2Pane::rebuild_wires(ImVec2 canvas_origin) { | |
| if (!wires_dirty_) | |
| return; |
| paren_depth--; | ||
| if (paren_depth < 0) | ||
| return std::string("Mismatched ')' at position " + std::to_string(i)); | ||
| current += c; |
There was a problem hiding this comment.
split_args builds error strings using std::string("...") around an expression that starts with a string literal plus std::to_string(...). This is invalid C++ (const char* + std::string) and will not compile. Build the message with std::string("...") + std::to_string(...) (and similarly for the "Unclosed" cases).
| brace_depth--; | ||
| if (brace_depth < 0) | ||
| return std::string("Mismatched '}' at position " + std::to_string(i)); | ||
| current += c; |
There was a problem hiding this comment.
This error return uses std::string("...") around a const char* + std::string expression, which is invalid C++. Build the message as std::string("...") + std::to_string(i) (or similar).
| 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"); |
There was a problem hiding this comment.
These Unclosed error returns use the same invalid const char* + std::string concatenation pattern and won’t compile. Construct the string via std::string(...) + ... (or ostringstream).
| // Re-add to net's destinations | ||
| auto net = grab_undo_.old_entry ? grab_undo_.old_entry->as_net() : nullptr; | ||
| if (net) { | ||
| net->destinations().push_back(pin->node()); |
There was a problem hiding this comment.
do_reconnect_pin always pushes pin->node() into net->destinations(). If the pin being reconnected is an output/source pin, the correct restore action is to set net->source() back to the node, not add it as a destination.
| // Re-add to net's destinations | |
| auto net = grab_undo_.old_entry ? grab_undo_.old_entry->as_net() : nullptr; | |
| if (net) { | |
| net->destinations().push_back(pin->node()); | |
| // Re-add to net's destinations, but avoid adding the source node as a destination | |
| auto net = grab_undo_.old_entry ? grab_undo_.old_entry->as_net() : nullptr; | |
| if (net) { | |
| auto node = pin->node(); | |
| if (node) { | |
| auto src = net->source().lock(); | |
| if (!src || src != node) { | |
| net->destinations().push_back(node); | |
| } | |
| } |
| int source_pin = 0; | ||
|
|
||
| if (auto net = entry->as_net()) { | ||
| if (net->is_the_unconnected()) continue; | ||
| auto src_ptr = net->source().lock(); |
There was a problem hiding this comment.
source_pin is initialized to 0, but output index 0 is a valid value. Using if (source_pin == 0) as a “not found” check is ambiguous; initialize to -1 and check source_pin < 0 instead.
No description provided.