Imported from striderZA/Tiny-RF-Simulator (
node_graph/AGENTS.md). Install upstream withnpx skills add striderZA/Tiny-RF-Simulator --skill node_graph. Copyright stays with the author.
node_graph — AGENTS.md
Purpose
Own the node-graph model and its editor UI, plus the shared DSP rewire pass: the topology-only
NodeGraphEngine (nodes, links, probes, groups), the ImNodes-based NodeGraphWidget (rendering,
interaction, schematic symbols), and rewireComponentInputs().
Ownership
include/node_graph_engine.h—GraphNode,GraphLink,SignalSource,NodeGraphEngine, and the view-layerNodeKindenum withthemeColor()src/node_graph_engine.cpp— topology, probe, and group implementationinclude/node_graph_widget.h—NodeGraphWidgetsrc/node_graph_widget.cpp— node/link rendering, canvas menu, link creation/deletion, hoversrc/node_graph_widget_groups.cpp— group backgrounds, collapsed group blocks, rubber-band selection, group renamesrc/node_graph_widget_tooltips.cpp— pin, node, link, and collapsed-subcircuit hover tooltips (signal summary plus interaction hints)src/schematic_symbols.cpp— per-NodeKindschematic symbol drawinginclude/rewire.h/src/rewire.cpp—rewireComponentInputs(), the shared rewire loopCMakeLists.txt—simulator::node_graph_engine(pure data, no ImGui) andsimulator::node_graph_widget(ImNodes UI)
Local Contracts
- Engine/widget split:
node_graph_engineincludes no ImGui/ImNodes header and holds only topology, probes, groups, and id counters;node_graph_widgetowns all ImGui/ImNodes rendering and interaction. The engine never reads, stores, or returnsNodeKind: it is derived fromGraphNode::labelat render time (NodeGraphWidget::kindForLabel()), andthemeColor()returns a plainuint32_tARGB value (ImU32 bit layout) so the engine keeps no UI dependency. The label→NodeKindderivation resolves the longest matching label prefix, so overlapping prefixes (e.g.SPDT SwitchandSPDT Switch (2:1)) cannot depend on registry order. - The engine stays topology-only: it holds no physical link rule.
graphLinkAllowed()lives incommon/graph_link_policy.hand reaches creation/load paths throughNodeGraphWidget::onLinkCreating; the DSP pass that applies it isrewireComponentInputs(). void rewireComponentInputs(std::span<IComponentEngine *const> components, const NodeGraphEngine &graph)is the single rewire loop shared by the app and thetest_flowharness. For every component and input pinkit resolves the linked upstream output throughgraph.getSourceForInput(), gates the connection withgraphLinkAllowed(), and bindsnode().inputs[k]to&source.node->outputs[source.output_index], or tonullptrwhen the pin is unlinked, physically disallowed, or the resolved port index is out of range.RfSimulatorApp::rewireInputs()delegates to it, so the GUI and the harness cannot drift on what a circuit is wired to.- Groups (
Group,GroupBoundaryPinfromcommon/include/group.h) are a visual layer:NodeGraphEngineowns them andNodeGraphWidgetrenders/collapses them; no DSP engine consumes them. - Node, pin, link, group, and boundary-pin id counters are monotonic and restored on project load through
setNextIds(),setNextGroupId(), andsetNextBoundaryPinId(). - Node removal must re-run
rewireComponentInputs()synchronously withComponentRegistry::remove()so no surviving component holds a danglingSpectrum*into the destroyed engine'sSignalNode(issue #37). topologicalOrder()counts only links whose start and end pins both resolve to graph nodes; stale or dangling links are not treated as graph edges or cycles.NodeGraphEngine::canAddLink()— backed byinputHasLink()andwouldCreateCycle()— is the editing policy for link creation: it rejects a second link into an occupied input pin and any link that would close a directed cycle.addLink()itself stays permissive so callers that intentionally build multi-source inputs keep working; the app'sonLinkCreatingand the project loader both gate oncanAddLink(), so the GUI cannot build a circuit thetest_flowharness rejects.NodeGraphWidgetcaches each node's pan-independent grid position:drawNodes()refreshes it for visible nodes andcaptureGridPositions()snapshots every node after a project load. Collapsed group members stop being drawn and are dropped from the imnodes pool, sodrawGroupCollapsedBlocks()places each block from that cache (never from the per-frame screen-position map).drawLinks()treats "both endpoints hidden" as an internal link only when both belong to the same collapsed group; a link between two different collapsed groups is drawn through both groups' synthesized boundary pins.- Widget callbacks (
onNodeMoved,onRemoveNode,onDuplicateNode,onLinkChanged,onLinkCreating,onNodeHover) are the only channel from widget to app; the engine itself takes no app-level callbacks. - Hover tooltips are the discoverability surface for the editor's gestures, so the chords they print are the ones
NodeGraphWidget::handleProbeClick()implements — Ctrl+click adds a probe (the Spectrum Analyzer plots that signal), Shift+click removes it — and Help/Tutorial wording must match. A tooltip is suppressed while a mouse button is dragging and while a pin owns the hover, so a pin hover never stacks the node tooltip on top of the pin tooltip. The node probe hint reports the probe slot already held by the node's first output, the port a node-body Ctrl+click targets. - The probe hints are gated on the pin
handleProbeClick()would actually target. For a collapsed block that pin isNodeGraphEngine::firstOutputBoundaryPin()— the handler reads it too, so the two cannot drift — and consequently a group with no cross-boundary output link prints no probe hint, while a block whose boundary pin already holds a probe reports its slot instead of advertising a probe (input-only boundary links are not a target).
Work Guidance
- Add a component type = one
NodeKindenumerator plus athemeColor()case and aschematic_symbols.cppdrawing case; the app registers the label prefix throughNodeGraphWidget::registerNodeKind()from theComponentTypeRegistryrow'slabel_prefix+kind. - Keep
rewireComponentInputs()the only rewire implementation; do not add a second rewire loop inapp/ortest_flow/.
Verification
ctest --test-dir buildmust pass with zero failures.tests/test_node_graph_engine.cppcovers add/remove, link topology, probes, id counters, groups (firstOutputBoundaryPin()with no link, an input-only boundary link, an output boundary link, and after the link is removed), andthemeColor();tests/test_issue87_flow.cppcovers the shared link policy and rewire behavior.test_engine/ui_tests.cpp::hover_tooltips_node_link_subcircuitcovers the hover tooltips end-to-end: a node body, a link, and a collapsed subcircuit block must each raise a tooltip window (anyImGuiWindowFlags_Tooltipwindow, read throughWasActivebecause the test engine runs between frames), and an empty-canvas point must raise none; the collapsed-block case deliberately uses a group with no output boundary pin (asserted). That window-existence check does not read tooltip text — ImGui registers text items with id 0, so the test engine cannot query them — which means the hint text itself is covered byfirstOutputBoundaryPin()at the engine level rather than by this test.
Child DOX Index
No child docs. node_graph/ is a flat two-target module.