Extract duplicate into core; support it in the script engine + wx GUI
A script using `duplicate` failed with "unsupported command 'duplicate'"
because the clone logic was still inline in the tui command. Extract it to
core/app/edit.hpp::duplicate_module(System*, src, dst) -> {ok, error, parts,
signals}: a deep clone of a module (parts, pins with spec + nc_origin, signals
with type overrides, pin→signal wiring; no connections), refusing on an unknown
source or an already-taken destination name.
- the tui `duplicate` command renders the result (output unchanged);
- the script engine dispatches `duplicate` to it — the failing script now runs;
- the wx GUI gains Edit ▸ Duplicate module… (PickModule + a name prompt).
tests/test_edit.cpp: deep clone wires to the clone's own signal (not the
source's) and preserves the type; unknown source / existing destination
refused. 412 core assertions green; tui + wx build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
#include "core/app/edit.hpp"
|
||||
|
||||
#include "core/domain/bsdl_model.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pin_model.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
#include "core/domain/transform_vpx.hpp" // ValidatePartForKind
|
||||
|
||||
#include <exception>
|
||||
|
||||
namespace app {
|
||||
|
||||
SetConnectorTypeResult set_connector_type(Part *part, const std::string &kind)
|
||||
@@ -75,4 +80,58 @@ SetSignalTypeResult set_signal_type(Signal *sig, const std::string &type_name)
|
||||
return r;
|
||||
}
|
||||
|
||||
DuplicateResult duplicate_module(System *sys, const std::string &src_name,
|
||||
const std::string &dst_name)
|
||||
{
|
||||
DuplicateResult r;
|
||||
if (!sys) { r.error = "no system"; return r; }
|
||||
|
||||
Module *src;
|
||||
try { src = sys->modules()->get(src_name); }
|
||||
catch (const std::exception &) {
|
||||
r.error = "unknown module: " + src_name;
|
||||
return r;
|
||||
}
|
||||
if (sys->modules()->exists(dst_name)) {
|
||||
r.error = "duplicate refused: module '" + dst_name + "' already exists.";
|
||||
return r;
|
||||
}
|
||||
|
||||
Module *dst = new Module(dst_name);
|
||||
|
||||
// Signals first (preserve type overrides), so pins can re-wire to them.
|
||||
for (auto &skv : *src->signals) {
|
||||
Signal *ss = skv.second;
|
||||
Signal *ds = new Signal(ss->name);
|
||||
ds->type = ss->type;
|
||||
dst->signals->add(ds);
|
||||
}
|
||||
|
||||
// Parts, pins (spec + nc_origin), and the pin→signal wiring.
|
||||
for (auto &pkv : *src) {
|
||||
Part *sp = pkv.second;
|
||||
Part *dp = new Part(sp->name);
|
||||
dp->connector_type = sp->connector_type;
|
||||
for (auto &nkv : *sp) {
|
||||
Pin *sn = nkv.second;
|
||||
Pin *dn = new Pin(sn->name);
|
||||
dn->spec = sn->spec;
|
||||
dn->nc_origin = sn->nc_origin;
|
||||
dp->add(dn);
|
||||
if (sn->signal()) {
|
||||
Signal *ds = dst->signals->get(sn->signal()->name);
|
||||
ds->add(dn);
|
||||
dn->connect(ds);
|
||||
}
|
||||
}
|
||||
dst->add(dp);
|
||||
}
|
||||
|
||||
sys->modules()->add(dst);
|
||||
r.parts = (int)dst->size();
|
||||
r.signals = (int)dst->signals->size();
|
||||
r.ok = true;
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
class Part;
|
||||
class Signal;
|
||||
class System;
|
||||
|
||||
// Application layer: UI-independent part-editing operations any frontend can
|
||||
// call. No console, no dialogs, no FTXUI — Part in, result struct out.
|
||||
@@ -52,6 +53,21 @@ struct SetSignalTypeResult {
|
||||
// Fails (ok=false, error set, no mutation) on an unrecognised name.
|
||||
SetSignalTypeResult set_signal_type(Signal *sig, const std::string &type_name);
|
||||
|
||||
// Outcome of cloning a module under a new name.
|
||||
struct DuplicateResult {
|
||||
bool ok = false;
|
||||
std::string error; ///< unknown source, or destination name already taken
|
||||
int parts = 0;
|
||||
int signals = 0;
|
||||
};
|
||||
|
||||
// Deep-clone module `src_name` as `dst_name`: parts, pins (spec + nc_origin),
|
||||
// signals (with type overrides) and the pin→signal wiring — but not the
|
||||
// system's connections. Fails (ok=false, error set, no change) when the source
|
||||
// is unknown or the destination name already exists.
|
||||
DuplicateResult duplicate_module(System *sys, const std::string &src_name,
|
||||
const std::string &dst_name);
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif // _APP_EDIT_HPP_
|
||||
|
||||
@@ -248,6 +248,15 @@ private:
|
||||
+ " ports bound" + (r.unbound ? (", " + std::to_string(r.unbound) + " unbound") : ""));
|
||||
return true;
|
||||
}
|
||||
if (cmd == "duplicate") {
|
||||
if (!need(2)) return false;
|
||||
DuplicateResult r = duplicate_module(sys_.get(), a[0], a[1]);
|
||||
if (!r.ok) { emit(r.error); return false; }
|
||||
emit("duplicate: '" + a[0] + "' → '" + a[1] + "' ("
|
||||
+ std::to_string(r.parts) + " part(s), "
|
||||
+ std::to_string(r.signals) + " signal(s))");
|
||||
return true;
|
||||
}
|
||||
if (cmd == "verify") {
|
||||
render_verify();
|
||||
return true;
|
||||
|
||||
@@ -595,51 +595,12 @@ void Tui::RegisterCommands() {
|
||||
{"new module name", Completion::None}},
|
||||
[this](const std::vector<std::string> &args) {
|
||||
if (!sys) { Print("no system: run 'new' first."); return; }
|
||||
|
||||
Module *src;
|
||||
try { src = sys->modules()->get(args[0]); }
|
||||
catch (const std::exception &) {
|
||||
Print("unknown module: " + args[0]); return;
|
||||
}
|
||||
if (sys->modules()->exists(args[1])) {
|
||||
Print("duplicate refused: module '" + args[1] + "' already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
Module *dst = new Module(args[1]);
|
||||
|
||||
// 1. Copy signals (preserve type overrides).
|
||||
for (auto &skv : *src->signals) {
|
||||
Signal *ss = skv.second;
|
||||
Signal *ds = new Signal(ss->name);
|
||||
ds->type = ss->type;
|
||||
dst->signals->add(ds);
|
||||
}
|
||||
|
||||
// 2. Copy parts, pins, and re-wire pin→signal.
|
||||
for (auto &pkv : *src) {
|
||||
Part *sp = pkv.second;
|
||||
Part *dp = new Part(sp->name);
|
||||
dp->connector_type = sp->connector_type;
|
||||
for (auto &nkv : *sp) {
|
||||
Pin *sn = nkv.second;
|
||||
Pin *dn = new Pin(sn->name);
|
||||
dn->spec = sn->spec;
|
||||
dn->nc_origin = sn->nc_origin;
|
||||
dp->add(dn);
|
||||
if (sn->signal()) {
|
||||
Signal *ds = dst->signals->get(sn->signal()->name);
|
||||
ds->add(dn);
|
||||
dn->connect(ds);
|
||||
}
|
||||
}
|
||||
dst->add(dp);
|
||||
}
|
||||
|
||||
sys->modules()->add(dst);
|
||||
app::DuplicateResult r =
|
||||
app::duplicate_module(sys.get(), args[0], args[1]);
|
||||
if (!r.ok) { Print(r.error); return; }
|
||||
Print("duplicate: '" + args[0] + "' → '" + args[1] + "'"
|
||||
+ " (" + std::to_string(dst->size()) + " part(s), "
|
||||
+ std::to_string(dst->signals->size()) + " signal(s))");
|
||||
+ " (" + std::to_string(r.parts) + " part(s), "
|
||||
+ std::to_string(r.signals) + " signal(s))");
|
||||
},
|
||||
/*prompt_for_missing=*/ true,
|
||||
"clone a module under a new name (parts, pins, signals; no connections)",
|
||||
|
||||
@@ -42,6 +42,7 @@ enum {
|
||||
ID_ATTACH_BSDL,
|
||||
ID_SET_SIGNAL_TYPE,
|
||||
ID_CONNECT,
|
||||
ID_DUPLICATE,
|
||||
ID_VERIFY,
|
||||
ID_QUIT,
|
||||
ID_ABOUT,
|
||||
@@ -132,6 +133,8 @@ EssimFrame::EssimFrame(WxFrontend &fe)
|
||||
edit->Append(ID_SET_SIGNAL_TYPE, "Set &signal type…\tCtrl-G");
|
||||
edit->AppendSeparator();
|
||||
edit->Append(ID_CONNECT, "C&onnect parts…\tCtrl-O");
|
||||
edit->AppendSeparator();
|
||||
edit->Append(ID_DUPLICATE, "&Duplicate module…\tCtrl-D");
|
||||
|
||||
auto *sysm = new wxMenu;
|
||||
sysm->Append(ID_VERIFY, "&Verify\tCtrl-K");
|
||||
@@ -195,6 +198,7 @@ EssimFrame::EssimFrame(WxFrontend &fe)
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnAttachBsdl, this, ID_ATTACH_BSDL);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnSetSignalType, this, ID_SET_SIGNAL_TYPE);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnConnect, this, ID_CONNECT);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnDuplicateModule, this, ID_DUPLICATE);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnVerify, this, ID_VERIFY);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnQuit, this, ID_QUIT);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnAbout, this, ID_ABOUT);
|
||||
@@ -594,6 +598,29 @@ void EssimFrame::OnSetSignalType(wxCommandEvent &) {
|
||||
RebuildModelView();
|
||||
}
|
||||
|
||||
void EssimFrame::OnDuplicateModule(wxCommandEvent &) {
|
||||
Module *m = PickModule("Duplicate module");
|
||||
if (!m) return;
|
||||
const std::string src = m->name; // m may move in the table after the add
|
||||
|
||||
wxTextEntryDialog dlg(this, "New module name:", "Duplicate module",
|
||||
wx(src) + "_copy");
|
||||
if (dlg.ShowModal() != wxID_OK) return;
|
||||
const std::string dst = dlg.GetValue().utf8_string();
|
||||
if (dst.empty()) return;
|
||||
|
||||
app::DuplicateResult r = app::duplicate_module(fe_.system(), src, dst);
|
||||
if (!r.ok) {
|
||||
Log(wx(r.error));
|
||||
wxMessageBox(wx(r.error), "Duplicate module", wxOK | wxICON_ERROR, this);
|
||||
return;
|
||||
}
|
||||
Log(wx("duplicate: '" + src + "' → '" + dst + "' ("
|
||||
+ std::to_string(r.parts) + " part(s), "
|
||||
+ std::to_string(r.signals) + " signal(s))"));
|
||||
RebuildModelView();
|
||||
}
|
||||
|
||||
void EssimFrame::OnVerify(wxCommandEvent &) {
|
||||
app::VerifyReport r = app::verify(fe_.system());
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ private:
|
||||
void OnAttachBsdl(wxCommandEvent &);
|
||||
void OnConnect(wxCommandEvent &);
|
||||
void OnSetSignalType(wxCommandEvent &);
|
||||
void OnDuplicateModule(wxCommandEvent &);
|
||||
void OnVerify(wxCommandEvent &);
|
||||
void OnQuit(wxCommandEvent &);
|
||||
void OnAbout(wxCommandEvent &);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "core/app/edit.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signal_type.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
// app::set_connector_type is pure core: validate the kind, tag the part and
|
||||
// apply the connector model. No Print/dialog/FTXUI.
|
||||
@@ -73,3 +75,39 @@ TEST_CASE("set_signal_type rejects an unknown name without mutating") {
|
||||
CHECK(r.error.find("power, gnd, other") != std::string::npos);
|
||||
CHECK(s.type == SignalType::Other); // unchanged
|
||||
}
|
||||
|
||||
TEST_CASE("duplicate_module deep-clones parts, pins and signals") {
|
||||
System sys;
|
||||
Module *a = sys.modules()->merge("A");
|
||||
Part *p = new Part("J1"); a->add(p);
|
||||
Pin *pin = new Pin("1"); p->add(pin);
|
||||
Signal *s = a->signals->merge("NET");
|
||||
s->type = SignalType::Power;
|
||||
s->add(pin); pin->connect(s);
|
||||
|
||||
app::DuplicateResult r = app::duplicate_module(&sys, "A", "B");
|
||||
|
||||
CHECK(r.ok);
|
||||
CHECK(r.parts == 1);
|
||||
CHECK(r.signals == 1);
|
||||
REQUIRE(sys.modules()->exists("B"));
|
||||
Module *b = sys.modules()->get("B");
|
||||
CHECK(b->signals->get("NET")->type == SignalType::Power); // type preserved
|
||||
Pin *bpin = b->get("J1")->get("1");
|
||||
REQUIRE(bpin->signal() != nullptr);
|
||||
CHECK(bpin->signal() == b->signals->get("NET")); // wired to the clone's own signal
|
||||
CHECK(bpin->signal() != s); // not aliasing the source
|
||||
}
|
||||
|
||||
TEST_CASE("duplicate_module refuses an unknown source or an existing destination") {
|
||||
System sys;
|
||||
sys.modules()->merge("A");
|
||||
|
||||
app::DuplicateResult dst = app::duplicate_module(&sys, "A", "A");
|
||||
CHECK_FALSE(dst.ok);
|
||||
CHECK(dst.error.find("already exists") != std::string::npos);
|
||||
|
||||
app::DuplicateResult unk = app::duplicate_module(&sys, "NOPE", "X");
|
||||
CHECK_FALSE(unk.ok);
|
||||
CHECK(unk.error.find("unknown module") != std::string::npos);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user