Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions adc/include/adc_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class AdcEngine : public IComponentEngine {

int id() const override { return m_id; }
int graphNodeId() const override { return m_graph_node_id; }
std::string_view type_name() const override { return "adc"; }
std::string hoverSummary() const override;
int inputPinId() const override;
int outputPinId() const override;
Expand Down
3 changes: 2 additions & 1 deletion adc/src/adc_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ nlohmann::json AdcEngine::serialize() const {
}

void AdcEngine::deserialize(const nlohmann::json &j) {
m_fs_Hz = j.value("sample_rate_Hz", 1e9);
m_fs_Hz =
j.contains("sample_rate_Hz") ? j["sample_rate_Hz"].get<double>() : j.value("fs_Hz", 1e9);
m_nsd_dBm_per_Hz = j.value("nsd_dBm_per_Hz", -155.0);
m_dirty = true;
}
Expand Down
1 change: 1 addition & 0 deletions amplifier/include/amplifier_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class AmplifierEngine : public IComponentEngine {
AmplifierEngine(int id, NodeGraphEngine &graph);
int id() const override { return m_id; }
int graphNodeId() const override { return m_graph_node_id; }
std::string_view type_name() const override { return "amplifier"; }
std::string hoverSummary() const override;
int inputPinId() const override;
int outputPinId() const override;
Expand Down
11 changes: 9 additions & 2 deletions amplifier/src/amplifier_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,16 @@ void AmplifierEngine::deserialize(const nlohmann::json &j) {
m_gain_dB = j.value("gain_dB", 0.0);
m_nf_dB = j.value("nf_dB", 0.0);
m_nonlinear.setEnabled(j.value("enable_nonlinear", false));
m_nonlinear.setOIP2_dBm(j.value("oip2_dBm", 50.0));
m_nonlinear.setOIP3_dBm(j.value("oip3_dBm", 50.0));
m_nonlinear.setOIP2_dBm(j.value("oip2_dBm", 100.0));
m_nonlinear.setOIP3_dBm(j.value("oip3_dBm", 100.0));
m_nonlinear.setP1dB_dBm(j.value("p1db_dBm", 100.0));
// Library definitions (schema v1/v2) omit `enable_nonlinear` but include
// OIP/P1dB params. The old registry factory enabled nonlinearity whenever
// any of those were present; project files always serialize the explicit
// key, so only fall back when it is absent.
if (!j.contains("enable_nonlinear") &&
(j.contains("oip2_dBm") || j.contains("oip3_dBm") || j.contains("p1db_dBm")))
m_nonlinear.setEnabled(true);
m_sparam_mode = j.value("sparam_mode", false);
m_sparam_filepath = j.value("sparam_filepath", "");
m_sparam_fwd_idx = j.value("sparam_fwd_idx", 0);
Expand Down
14 changes: 8 additions & 6 deletions app/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# app/AGENTS.md

## Purpose
Application orchestrator layer containing `RfSimulatorApp`, `ComponentRegistry`, and `InspectorPanel`.
Application orchestrator layer containing `RfSimulatorApp`, `ComponentRegistry`, `ComponentTypeRegistry`, `InspectorPanel`, `PFBViewManager`, and `ProjectSerializer`.

## Ownership
- `RfSimulatorApp` — application boot, frame loop, DSP update, UI orchestration, project save/load
- `RfSimulatorApp` — application boot, frame loop, DSP update, UI orchestration (project save/load logic lives in `ProjectSerializer`)
- `ComponentRegistry` — polymorphic component lifecycle and type-indexed lookup
- `InspectorPanel` — property editing panel with dirty tracking
- `ComponentTypeRegistry` — data-driven type schema table (field lists + factories) used by `ComponentLibrary::instantiate()`/`validate()` and the component authoring form
- `ComponentTypeRegistry` — single dispatch table (11 rows) for canvas menu, add, duplicate, save/load, and inspector drawing: each row carries the canonical `type` + `.rfsim` `project_type` keys, `menu_label`/`label_prefix`, `NodeKind`, a `create()` factory, and a `draw_inspector` callback; also drives `ComponentLibrary::instantiate()`/`validate()` and the component authoring form
- `PFBViewManager` — owns the per-PFB IQ Plot / Channelizer Grid widget lifecycle (replaces the app's four lockstep vectors `m_iq_widgets`/`m_show_iq_pfbs`/`m_pfb_grid_widgets`/`m_show_pfb_grids`, which were rebuilt by hand at six call sites and caused issue #37); all add/rebuild/clear/draw and visibility state funnel through this class
- `ProjectSerializer` — owns the `.rfsim` save/load/new JSON logic (extracted from `RfSimulatorApp`, issue #51)
- `ComponentFormModel` / `ComponentFormWidget` — pure-logic + ImGui rendering pair for the New/Edit Component form
- `ExtensionManager` — extension manifest discovery and status tracking across built-in/global/project-local roots
- `ExternalToolRunner` — structured request/result execution for approved external tools
## Local Contracts
- `saveProject()` / `loadProject()` / `newProject()` handle full project serialization to `.rfsim` JSON format
- `RfSimulatorApp::saveProject()` / `loadProject()` / `newProject()` are thin wrappers that delegate to `ProjectSerializer::save()` / `load()` / `reset()`; all `.rfsim` JSON serialization lives in `ProjectSerializer`
- Dirty tracking propagated via `markDirty()` / `onParamChange` / `onLinkChanged` callbacks
- File menu bar in `draw_ui()` handles keyboard shortcuts (`Ctrl+N`, `Ctrl+O`, `Ctrl+S`, `Ctrl+Shift+S`) and unsaved-changes modal; Help menu provides F1-toggled help window
- View menu's `Layouts` submenu (Save As.../Load/Manage...) drives `LayoutManager` (see `layout/AGENTS.md`) for named window-layout presets; the exe-relative default layout is auto-managed by ImGui itself via `IniFilename`, set in `core/src/core.cpp`
Expand All @@ -25,12 +27,12 @@ Application orchestrator layer containing `RfSimulatorApp`, `ComponentRegistry`,
- `draw_ui()` and `drawExtensionsPanel()` both dispatch through `externalToolActions()`; the Tools menu renders only actions whose `location == "tools"`, while the Extensions panel shows every declared action (or a single fallback Run button)
- App-level integration tests may use `testExtensionManager()` and `testExtensionResultMessage()` with an ImGui/ImPlot/ImNodes fixture
- `load_window_states()` runs on construction to restore persisted window visibility toggles
- Per-PFB IQ Plot / Channelizer Grid window visibility (`m_show_iq_pfbs`/`m_show_pfb_grids`, indexed in lockstep with `m_pfb_ptrs`) has no View-menu entry since instances are dynamic; closed windows are reopened via "Show IQ Plot"/"Show Channelizer Grid" checkboxes in the PFB properties panel, wired each frame through `InspectorPanel::setPFBWindowVisibility()` (stores the stable vector pointers, not element pointers, since the vectors are rebuilt on add/remove)
- Per-PFB IQ Plot / Channelizer Grid window visibility lives in `PFBViewManager::iqVisibility()`/`gridVisibility()` (indexed in lockstep with the manager's widgets) and has no View-menu entry since instances are dynamic; closed windows are reopened via "Show IQ Plot"/"Show Channelizer Grid" checkboxes in the PFB properties panel, wired each frame through `InspectorPanel::setPFBWindowVisibility()` (stores the stable vector pointers, not element pointers, since the vectors are rebuilt on add/remove)
- `update_dsp()`'s signal-routing pass is factored into `rewireInputs()` (sets every component's `node().inputs[k]` from current graph links, nulling severed ones); `onRemoveNode` calls it synchronously right after `ComponentRegistry::remove()` so no surviving component is left holding a dangling `Spectrum*` into the just-destroyed engine's `SignalNode` while the rest of that frame's `draw_ui()` still runs — widgets that dereference `node().inputs[]` directly during draw (e.g. `PFBChannelizerWidget`) would otherwise use-after-free (issue #37)
- Destructor saves window state via `SessionState`

## Work Guidance
- Add new component serialization in both `saveProject()` (dump to JSON) and `loadProject()` (read from JSON + create via `ComponentRegistry`)
- Add a new component = one `ComponentTypeRegistry` row (`type`, `project_type`, `menu_label`, `label_prefix`, `kind`, `create`, `draw_inspector`) + a `NodeKind`/symbol entry in `node_graph` — the menu, add, duplicate, save/load, inspector, and form paths all dispatch through the registry, so no per-file edits in `RfSimulatorApp` are needed

## Verification
- Round-trip tests in `tests/test_project_file.cpp`
Expand Down
2 changes: 2 additions & 0 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ add_library(app STATIC
src/extension_manifest.cpp
src/extension_manager.cpp
src/external_tool_runner.cpp
src/pfb_view_manager.cpp
src/project_serializer.cpp
)
add_library(simulator::app ALIAS app)

Expand Down
13 changes: 9 additions & 4 deletions app/include/app.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
#include "node_graph_widget.h"
#include "pfb_channelizer_engine.h"
#include "pfb_channelizer_widget.h"
#include "pfb_view_manager.h"
#include "project_serializer.h"
#include "session_state.h"
#include "signal_generator_engine.h"
#include "signal_generator_widget.h"
Expand Down Expand Up @@ -93,6 +95,7 @@ class RfSimulatorApp {
void load_window_states();
void rewireInputs();
void duplicateComponent(int graph_node_id);
void addComponent(const ComponentTypeDescriptor *desc, ImVec2 pos);
void openNewComponentForm(const std::string &type);
void openEditComponentForm(const ComponentDefinition &def);
void drawComponentFormModal();
Expand All @@ -104,9 +107,6 @@ class RfSimulatorApp {
std::unique_ptr<NodeGraphWidget> m_graph_widget;

std::vector<std::unique_ptr<SignalGeneratorWidget>> m_generator_widgets;
std::vector<std::unique_ptr<IQPlotWidget>> m_iq_widgets;
std::vector<bool> m_show_iq_pfbs;
std::vector<std::unique_ptr<PFBChannelizerWidget>> m_pfb_grid_widgets;
ComponentLibrary m_library;
std::unique_ptr<LibraryBrowserWidget> m_library_browser;
bool m_show_library = false;
Expand All @@ -116,10 +116,15 @@ class RfSimulatorApp {
std::unique_ptr<ComponentFormModel> m_component_form_model;
std::unique_ptr<ComponentFormWidget> m_component_form_widget;
std::string m_component_form_error;
std::vector<bool> m_show_pfb_grids;
std::unique_ptr<InspectorPanel> m_inspector_panel;

ComponentRegistry m_components;
// Declared after m_components so the manager (and its widget references to
// engines) is destroyed before the engines themselves.
PFBViewManager m_pfb_views;
// Owns .rfsim save/load/new; declared after m_graph_widget and m_pfb_views
// so it is destroyed before them (it holds references to both).
std::unique_ptr<ProjectSerializer> m_serializer;
int m_next_component_id = 100;
PendingAction m_pending_action = PendingAction::None;
bool m_show_unsaved_dialog = false;
Expand Down
32 changes: 22 additions & 10 deletions app/include/component_type_registry.h
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
// app/include/component_type_registry.h
#pragma once

#include "node_graph_engine.h"
#include <functional>
#include <limits>
#include <nlohmann/json.hpp>
#include <string>
#include <string_view>
#include <vector>

class ComponentRegistry;
class NodeGraphEngine;
class IComponentEngine;
class InspectorPanel;

enum class FieldKind { Number, String, Enum, FilePath, Bool };

Expand All @@ -27,21 +29,31 @@ struct ParameterField {
};

struct ComponentTypeDescriptor {
std::string type; // "amplifier"
std::string display_name; // "Amplifier"
std::vector<ParameterField> fields;
std::string type; // canonical key, e.g. "amplifier"
std::string project_type; // .rfsim save/load name, e.g. "Amplifier"
std::string display_name; // e.g. "Amplifier"
std::string menu_label; // canvas menu item, e.g. "Add Amplifier"
std::string label_prefix; // graph label prefix, e.g. "Amplifier"
NodeKind kind = NodeKind::Unknown;
bool authorable = false; // appears in New Component form combo
bool supports_sparam_file = false;
std::function<IComponentEngine *(ComponentRegistry &, NodeGraphEngine &, int,
const nlohmann::json &)>
factory;
std::vector<ParameterField> fields;

// Create a default engine of this type (no params). Callers apply params
// via engine->deserialize().
std::function<IComponentEngine *(ComponentRegistry &, NodeGraphEngine &, int)> create;
// Inspector property draw. Receives the panel so PFB's multi-instance
// selector and dirty-flag state stay reachable.
std::function<void(InspectorPanel &, IComponentEngine &)> draw_inspector;
};

class ComponentTypeRegistry {
public:
static const ComponentTypeRegistry &instance();
static ComponentTypeRegistry &instance();

const ComponentTypeDescriptor *find(const std::string &type) const;
std::vector<const ComponentTypeDescriptor *> all() const;
const ComponentTypeDescriptor *find(std::string_view type) const;
const ComponentTypeDescriptor *findByProjectType(std::string_view name) const;
std::vector<ComponentTypeDescriptor *> all();

private:
ComponentTypeRegistry();
Expand Down
59 changes: 31 additions & 28 deletions app/include/inspector_panel.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "component_interface.h"
#include "component_type_registry.h"
#include "node_graph_engine.h"
#include "signal_node.h"
#include <functional>
Expand Down Expand Up @@ -35,9 +36,17 @@ class InspectorPanel {

void draw(const char *title, bool *p_open = nullptr);
void setPFBs(const std::vector<PFBChannelizerEngine *> &pfbs) {
const bool selection_survives =
m_selected_pfb_index >= 0 && m_selected_pfb_index < static_cast<int>(pfbs.size()) &&
m_selected_pfb_index < static_cast<int>(m_pfb_ptrs.size()) &&
pfbs[m_selected_pfb_index] == m_pfb_ptrs[m_selected_pfb_index];
m_pfb_ptrs = pfbs;
if (m_selected_pfb_index >= static_cast<int>(m_pfb_ptrs.size()))
m_selected_pfb_index = std::max(0, static_cast<int>(m_pfb_ptrs.size()) - 1);
// A PFB was added/removed and the combo selection did not survive the
// reshuffle: drop the anchor so draw() re-follows the graph selection.
if (!selection_survives)
m_pfb_combo_graph_id = -1;
}
// Vectors are owned by the caller (RfSimulatorApp) and stay stable across frames;
// only their contents are rebuilt on add/remove, so storing the vector pointers
Expand All @@ -52,46 +61,40 @@ class InspectorPanel {
void setViewToggles(const ViewToggles &t) { m_viewToggles = t; }
bool m_param_edited = false;

// Called once at startup; wires ComponentTypeRegistry draw_inspector
// callbacks to this panel's property drawers.
void registerDrawers(ComponentTypeRegistry &registry);

void drawAmplifierProperties(AmplifierEngine &engine, int index);
void drawCoaxCableProperties(CoaxCableEngine &engine, int index);
void drawEqualizerProperties(EqualizerEngine &engine, int index);
void drawMixerProperties(MixerEngine &engine, int index);
void drawSplitterProperties(SplitterEngine &engine, int index);
void drawAdcProperties(AdcEngine &engine, int index);
void drawGeneratorProperties(SignalGeneratorEngine &engine, int index);
void drawPFBProperties(PFBChannelizerEngine &engine);
void drawIdealFilterProperties(IdealFilterEngine &engine, int index);
void drawAttenuatorProperties(AttenuatorEngine &engine, int index);
void drawCombinerProperties(CombinerEngine &engine, int index);
void drawGroupPanel(int group_id);

private:
NodeGraphEngine &m_graph;
ComponentRegistry *m_components = nullptr;
std::vector<PFBChannelizerEngine *> m_pfb_ptrs;
int m_selected_pfb_index = 0;
// Graph-selected PFB id that the combo selection is anchored to; -1 means
// draw() must re-follow the graph selection (initial state, or after the
// PFB set changed underneath the selection).
int m_pfb_combo_graph_id = -1;
ViewToggles m_viewToggles;
std::vector<bool> *m_pfb_iq_visible = nullptr;
std::vector<bool> *m_pfb_grid_visible = nullptr;

enum class ComponentType {
None,
Generator,
Amplifier,
Splitter,
Mixer,
Adc,
PFB,
IdealFilter,
CoaxCable,
Equalizer,
Attenuator,
Combiner
};
struct Hit {
ComponentType type;
const ComponentTypeDescriptor *desc = nullptr;
IComponentEngine *engine = nullptr;
};
Hit findSelected() const;
std::string labelForHit(const Hit &hit) const;

void drawAmplifierProperties(AmplifierEngine &engine, int index);
void drawCoaxCableProperties(CoaxCableEngine &engine, int index);
void drawEqualizerProperties(EqualizerEngine &engine, int index);
void drawMixerProperties(MixerEngine &engine, int index);
void drawSplitterProperties(SplitterEngine &engine, int index);
void drawAdcProperties(AdcEngine &engine, int index);
void drawGeneratorProperties(SignalGeneratorEngine &engine, int index);
void drawPFBProperties(PFBChannelizerEngine &engine);
void drawIdealFilterProperties(IdealFilterEngine &engine, int index);
void drawAttenuatorProperties(AttenuatorEngine &engine, int index);
void drawCombinerProperties(CombinerEngine &engine, int index);
void drawGroupPanel(int group_id);
};
32 changes: 32 additions & 0 deletions app/include/pfb_view_manager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#pragma once

#include "iq_plot_widget.h"
#include "pfb_channelizer_engine.h"
#include "pfb_channelizer_widget.h"
#include <memory>
#include <vector>

class ComponentRegistry;
class SessionState;

// Owns the per-PFB view widgets and their visibility flags. The app's old
// four lockstep vectors (m_iq_widgets/m_show_iq_pfbs/m_pfb_grid_widgets/
// m_show_pfb_grids) were rebuilt by hand at six call sites and caused issue
// #37 (use-after-free). All lifecycle now funnels through this class.
class PFBViewManager {
public:
void addFor(PFBChannelizerEngine &engine, SessionState &state);
void rebuild(const ComponentRegistry &components, SessionState &state);
void clear();
void draw();
void saveVisibility(const ComponentRegistry &components, SessionState &state) const;

std::vector<bool> &iqVisibility() { return m_show_iq_pfbs; }
std::vector<bool> &gridVisibility() { return m_show_pfb_grids; }

private:
std::vector<std::unique_ptr<IQPlotWidget>> m_iq_widgets;
std::vector<bool> m_show_iq_pfbs;
std::vector<std::unique_ptr<PFBChannelizerWidget>> m_pfb_grid_widgets;
std::vector<bool> m_show_pfb_grids;
};
35 changes: 35 additions & 0 deletions app/include/project_serializer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include <string>

class ComponentRegistry;
class NodeGraphEngine;
class NodeGraphWidget;
class PFBViewManager;
class SessionState;

// Owns the .rfsim JSON save/load/new logic previously inlined in
// RfSimulatorApp (issue #51: 1320-line god-object).
class ProjectSerializer {
public:
ProjectSerializer(ComponentRegistry &components, NodeGraphEngine &graph,
NodeGraphWidget &graph_widget, PFBViewManager &pfb_views, SessionState &state,
int &next_component_id, bool &show_log, bool &show_spectrum,
bool &show_properties, bool &show_node_editor);

void save(const std::string &path);
bool load(const std::string &path); // false on parse/unknown-type failure (logged)
void reset(); // newProject: links, components, probes, counters, PFBs

private:
ComponentRegistry &m_components;
NodeGraphEngine &m_graph;
NodeGraphWidget &m_graph_widget;
PFBViewManager &m_pfb_views;
SessionState &m_state;
int &m_next_component_id;
bool &m_show_log;
bool &m_show_spectrum;
bool &m_show_properties;
bool &m_show_node_editor;
};
Loading
Loading