From 7e88f824462a6a8089de4ef3a333cac1db9b5f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Wed, 3 Jun 2026 21:13:08 +0200 Subject: [PATCH] Extract set-connector-type into core; add it to the wx GUI. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First of the editing ops to reach the wx frontend. Extract the business logic (validate the kind, tag the part, apply the connector model) into core/app/edit.{hpp,cpp}: app::set_connector_type(Part*, kind) -> {ok, error, materialised}, refusing without mutation when the kind is invalid for the part. Both TUI call sites now use it: the `set-connector-type` command and the interactive settype screen (de-dup) — output unchanged. The wx GUI gains an Edit ▸ Set connector type… menu: a reusable PickPart() (module → part choice dialogs) + a kind prompt, then the same core op, logged and reflected in the model tree. Prune the now-dead pin_model/transform_vpx includes from commands.cpp. Unit-tested by tests/test_edit.cpp (free-form kind tags; invalid kind refused without mutation; null part). tui + wx build clean; 376 core assertions green. Co-Authored-By: Claude Opus 4.8 --- src/core/app/edit.cpp | 31 ++++++++++++++ src/core/app/edit.hpp | 27 ++++++++++++ src/frontends/tui/commands.cpp | 16 +++---- src/frontends/tui/screen_settype.cpp | 12 ++---- src/frontends/wx/wx_frame.cpp | 63 ++++++++++++++++++++++++++++ src/frontends/wx/wx_frame.hpp | 5 +++ tests/test_edit.cpp | 39 +++++++++++++++++ 7 files changed, 175 insertions(+), 18 deletions(-) create mode 100644 src/core/app/edit.cpp create mode 100644 src/core/app/edit.hpp create mode 100644 tests/test_edit.cpp diff --git a/src/core/app/edit.cpp b/src/core/app/edit.cpp new file mode 100644 index 0000000..d039d2d --- /dev/null +++ b/src/core/app/edit.cpp @@ -0,0 +1,31 @@ +#include "core/app/edit.hpp" + +#include "core/domain/parts.hpp" +#include "core/domain/pin_model.hpp" +#include "core/domain/transform_vpx.hpp" // ValidatePartForKind + +namespace app { + +SetConnectorTypeResult set_connector_type(Part *part, const std::string &kind) +{ + SetConnectorTypeResult r; + if (!part) { + r.error = "no part"; + return r; + } + + std::string err = ValidatePartForKind(part, kind); + if (!err.empty()) { + r.error = err; + return r; + } + + part->connector_type = kind; + ConnectorModel model(kind); + ApplyReport rep = apply_model(part, model); + r.materialised = rep.materialised; + r.ok = true; + return r; +} + +} // namespace app diff --git a/src/core/app/edit.hpp b/src/core/app/edit.hpp new file mode 100644 index 0000000..bfe43f7 --- /dev/null +++ b/src/core/app/edit.hpp @@ -0,0 +1,27 @@ +#ifndef _APP_EDIT_HPP_ +#define _APP_EDIT_HPP_ + +#include + +class Part; + +// Application layer: UI-independent part-editing operations any frontend can +// call. No console, no dialogs, no FTXUI — Part in, result struct out. +namespace app { + +// Outcome of tagging a part's connector type. The op validates the kind, sets +// the type and applies the connector model (which may materialise the layout's +// missing NC pins); the caller renders the result. +struct SetConnectorTypeResult { + bool ok = false; + std::string error; ///< set when refused (kind invalid for the part) + int materialised = 0; ///< NC pins created from the connector layout +}; + +// Tag `part`'s connector type and apply the matching connector model. Refuses +// (ok=false, error set, no mutation) when the kind is invalid for the part. +SetConnectorTypeResult set_connector_type(Part *part, const std::string &kind); + +} // namespace app + +#endif // _APP_EDIT_HPP_ diff --git a/src/frontends/tui/commands.cpp b/src/frontends/tui/commands.cpp index 4dcd472..761a9f0 100644 --- a/src/frontends/tui/commands.cpp +++ b/src/frontends/tui/commands.cpp @@ -7,16 +7,15 @@ #include "core/domain/parts.hpp" #include "core/domain/persist.hpp" #include "core/domain/pin_role.hpp" -#include "core/domain/pin_model.hpp" #include "core/domain/bsdl_model.hpp" #include "core/domain/pins.hpp" #include "core/domain/signals.hpp" #include "core/domain/system.hpp" #include "core/app/connect.hpp" +#include "core/app/edit.hpp" #include "core/app/load.hpp" #include "core/app/verify.hpp" -#include "core/domain/transform_vpx.hpp" // ValidatePartForKind #include #include @@ -397,18 +396,15 @@ void Tui::RegisterCommands() { return; } } - std::string err = ValidatePartForKind(prt, args[2]); - if (!err.empty()) { - Print("set-connector-type refused: " + err); + app::SetConnectorTypeResult r = app::set_connector_type(prt, args[2]); + if (!r.ok) { + Print("set-connector-type refused: " + r.error); return; } - prt->connector_type = args[2]; - ConnectorModel model(args[2]); - ApplyReport rep = apply_model(prt, model); Print(mod->name + "/" + prt->name + ": connector_type = " + (args[2].empty() ? "(none)" : args[2])); - if (rep.materialised > 0) - Print("set-connector-type: added " + std::to_string(rep.materialised) + if (r.materialised > 0) + Print("set-connector-type: added " + std::to_string(r.materialised) + " NC pin(s) from the connector layout"); }, /*prompt_for_missing=*/ false, diff --git a/src/frontends/tui/screen_settype.cpp b/src/frontends/tui/screen_settype.cpp index 1f45125..e794ce8 100644 --- a/src/frontends/tui/screen_settype.cpp +++ b/src/frontends/tui/screen_settype.cpp @@ -1,13 +1,12 @@ #include "frontends/tui/tui.hpp" #include "frontends/tui/tui_helpers.hpp" +#include "core/app/edit.hpp" #include "core/domain/modules.hpp" #include "core/domain/parts.hpp" -#include "core/domain/pin_model.hpp" #include "core/domain/pin_role.hpp" #include "core/domain/pins.hpp" #include "core/domain/system.hpp" -#include "core/domain/transform_vpx.hpp" #include #include @@ -63,14 +62,11 @@ Component Tui::BuildSettypeScreen() { try { Module *mod = sys->modules()->get(settype_modules[settype_m_idx]); Part *prt = mod->get(settype_p_list[settype_p_idx]); - std::string err = ValidatePartForKind(prt, settype_type); - if (!err.empty()) { - settype_status = "refused: " + err; + app::SetConnectorTypeResult r = app::set_connector_type(prt, settype_type); + if (!r.ok) { + settype_status = "refused: " + r.error; return; } - prt->connector_type = settype_type; - ConnectorModel model(settype_type); - apply_model(prt, model); std::string msg = mod->name + "/" + prt->name + " = " + (settype_type.empty() ? "(none)" : settype_type); settype_status = "applied: " + msg; diff --git a/src/frontends/wx/wx_frame.cpp b/src/frontends/wx/wx_frame.cpp index f7b0776..0b08e4b 100644 --- a/src/frontends/wx/wx_frame.cpp +++ b/src/frontends/wx/wx_frame.cpp @@ -2,6 +2,7 @@ #include "frontends/wx/wx_frontend.hpp" +#include "core/app/edit.hpp" #include "core/app/export.hpp" #include "core/app/load.hpp" #include "core/app/verify.hpp" @@ -31,6 +32,7 @@ enum { ID_RESTORE, ID_SAVE, ID_EXPORT, + ID_SET_CONNECTOR_TYPE, ID_VERIFY, ID_QUIT, ID_ABOUT, @@ -53,6 +55,9 @@ EssimFrame::EssimFrame(WxFrontend &fe) file->AppendSeparator(); file->Append(ID_QUIT, "&Quit\tCtrl-Q"); + auto *edit = new wxMenu; + edit->Append(ID_SET_CONNECTOR_TYPE, "Set &connector type…\tCtrl-T"); + auto *sysm = new wxMenu; sysm->Append(ID_VERIFY, "&Verify\tCtrl-K"); @@ -61,6 +66,7 @@ EssimFrame::EssimFrame(WxFrontend &fe) auto *bar = new wxMenuBar; bar->Append(file, "&File"); + bar->Append(edit, "&Edit"); bar->Append(sysm, "&System"); bar->Append(help, "&Help"); SetMenuBar(bar); @@ -93,6 +99,7 @@ EssimFrame::EssimFrame(WxFrontend &fe) Bind(wxEVT_MENU, &EssimFrame::OnRestore, this, ID_RESTORE); Bind(wxEVT_MENU, &EssimFrame::OnSave, this, ID_SAVE); Bind(wxEVT_MENU, &EssimFrame::OnExport, this, ID_EXPORT); + Bind(wxEVT_MENU, &EssimFrame::OnSetConnectorType, this, ID_SET_CONNECTOR_TYPE); Bind(wxEVT_MENU, &EssimFrame::OnVerify, this, ID_VERIFY); Bind(wxEVT_MENU, &EssimFrame::OnQuit, this, ID_QUIT); Bind(wxEVT_MENU, &EssimFrame::OnAbout, this, ID_ABOUT); @@ -246,6 +253,62 @@ void EssimFrame::OnExport(wxCommandEvent &) { } } +Part *EssimFrame::PickPart() { + System *sys = fe_.system(); + if (!sys || sys->modules()->size() == 0) { + wxMessageBox("No modules loaded.", "Select part", + wxOK | wxICON_INFORMATION, this); + return nullptr; + } + + std::vector mods; + for (auto &mkv : *sys->modules()) mods.push_back(mkv.first); + std::sort(mods.begin(), mods.end()); + wxArrayString mchoices; + for (const auto &m : mods) mchoices.Add(wx(m)); + int mi = wxGetSingleChoiceIndex("Module:", "Select part", mchoices, this); + if (mi < 0) return nullptr; + Module *m = sys->modules()->get(mods[mi]); + + if (m->size() == 0) { + wxMessageBox("That module has no parts.", "Select part", + wxOK | wxICON_INFORMATION, this); + return nullptr; + } + std::vector parts; + for (auto &pkv : *m) parts.push_back(pkv.first); + std::sort(parts.begin(), parts.end()); + wxArrayString pchoices; + for (const auto &p : parts) pchoices.Add(wx(p)); + int pi = wxGetSingleChoiceIndex("Part:", "Select part", pchoices, this); + if (pi < 0) return nullptr; + return m->get(parts[pi]); +} + +void EssimFrame::OnSetConnectorType(wxCommandEvent &) { + Part *p = PickPart(); + if (!p) return; + + wxTextEntryDialog dlg(this, "Connector type (empty = none):", + "Set connector type", wx(p->connector_type)); + if (dlg.ShowModal() != wxID_OK) return; + const std::string kind = dlg.GetValue().utf8_string(); + + app::SetConnectorTypeResult r = app::set_connector_type(p, kind); + if (!r.ok) { + Log("set-connector-type refused: " + wx(r.error)); + wxMessageBox(wx(r.error), "Refused", wxOK | wxICON_ERROR, this); + return; + } + + wxString who = (p->prnt ? wx(p->prnt->name) + "/" : wxString()) + wx(p->name); + Log(who + ": connector_type = " + (kind.empty() ? wxString("(none)") : wx(kind))); + if (r.materialised > 0) + Log(wxString::Format(" added %d NC pin(s) from the connector layout", + r.materialised)); + RebuildModelView(); +} + void EssimFrame::OnVerify(wxCommandEvent &) { app::VerifyReport r = app::verify(fe_.system()); diff --git a/src/frontends/wx/wx_frame.hpp b/src/frontends/wx/wx_frame.hpp index 31fcc37..6121b4a 100644 --- a/src/frontends/wx/wx_frame.hpp +++ b/src/frontends/wx/wx_frame.hpp @@ -22,10 +22,15 @@ private: void OnRestore(wxCommandEvent &); void OnSave(wxCommandEvent &); void OnExport(wxCommandEvent &); + void OnSetConnectorType(wxCommandEvent &); void OnVerify(wxCommandEvent &); void OnQuit(wxCommandEvent &); void OnAbout(wxCommandEvent &); + // Prompt the user to pick a module then a part from the current System. + // Returns nullptr if there is nothing to pick or the user cancels. + class Part *PickPart(); + void RebuildModelView(); ///< refresh tree + overview from the System void Log(const wxString &line); ///< append a line to the log pane diff --git a/tests/test_edit.cpp b/tests/test_edit.cpp new file mode 100644 index 0000000..a4e891c --- /dev/null +++ b/tests/test_edit.cpp @@ -0,0 +1,39 @@ +#include + +#include "core/app/edit.hpp" +#include "core/domain/parts.hpp" +#include "core/domain/pins.hpp" + +// app::set_connector_type is pure core: validate the kind, tag the part and +// apply the connector model. No Print/dialog/FTXUI. + +TEST_CASE("set_connector_type tags a part with a free-form kind") { + Part p("J1"); + p.add(new Pin("1")); + p.add(new Pin("2")); + + app::SetConnectorTypeResult r = app::set_connector_type(&p, "myconn"); + + CHECK(r.ok); + CHECK(r.error.empty()); + CHECK(p.connector_type == "myconn"); +} + +TEST_CASE("set_connector_type refuses a kind the part doesn't fit — no mutation") { + Part p("J1"); + p.add(new Pin("1")); + p.add(new Pin("2")); + p.add(new Pin("3")); // numeric pins don't fit the VPX single-letter columns + + app::SetConnectorTypeResult r = app::set_connector_type(&p, "vpx-3u-bkp-p0"); + + CHECK_FALSE(r.ok); + CHECK_FALSE(r.error.empty()); + CHECK(p.connector_type.empty()); // refused before any change +} + +TEST_CASE("set_connector_type on a null part fails cleanly") { + app::SetConnectorTypeResult r = app::set_connector_type(nullptr, "x"); + CHECK_FALSE(r.ok); + CHECK_FALSE(r.error.empty()); +}