From e345d4e5d01c3cdc5142795a8fc3ad455fae68c5 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 2 Oct 2026 22:30:49 +1300 Subject: [PATCH] Give each end of the wiring export its terminal index and uuid Most shipped symbols leave their terminals unnamed, so --export-wiring wrote both terminal columns empty and a reader had to work each end out from the .qet: terminal uuid, then its place in the definition, then the index. The terminal table now keeps the index the scripting API's addConductor() takes, from both fills of the database (the document and the folios give the same value: Element::parseTerminal()'s sort, top to bottom then left to right), and wiring_list_view and the CSV gain from/to_terminal_index and from/to_terminal_uuid as their last columns. The index is left empty for two terminals at one point, where the sort is not stable and the index is not defined; the uuid tells them apart. The wiring list dialog reads named columns and is unaffected. Co-Authored-By: Claude Opus 5.5 (1M context) --- misc/qet-mcp/README.md | 9 ++++ misc/qet-mcp/test_qet_mcp.py | 39 ++++++++++++++ sources/cli_export.cpp | 9 +++- sources/dataBase/projectdatabase.cpp | 76 +++++++++++++++++++++++++--- sources/dataBase/projectdatabase.h | 2 +- 5 files changed, 127 insertions(+), 8 deletions(-) diff --git a/misc/qet-mcp/README.md b/misc/qet-mcp/README.md index cd53ec880..f86491c72 100644 --- a/misc/qet-mcp/README.md +++ b/misc/qet-mcp/README.md @@ -542,6 +542,15 @@ Python, plus the hang guard on `addConductor` and the database refresh in `qet.terminalIndex()` in the build. A symbol file saved without terminal uuids lists them empty; QElectroTech gives the terminals of every project's copy of it a uuid on opening (#1118), written on the next save. +- **The `wiring` export names unnamed terminals.** Most shipped symbols + leave their terminals unnamed, so `from_terminal`/`to_terminal` are often + empty. Each row also ends with `from_terminal_index`, + `from_terminal_uuid`, `to_terminal_index` and `to_terminal_uuid`: the + index `add_conductor` takes, and the uuid the `.qet` names the terminal + by (as `qet_edit` accepts in place of the index). The index is empty for + a terminal sharing its point with another, where the order is undefined; + the uuid tells those apart. `wiring_list_view` carries the same four + columns for `qet_query`. - **`qet_export` isolates its launch.** SingleApplication keys its socket on `applicationFilePath()`, so a second launch of the same binary path forwards its request to an already-running instance and returns *that* diff --git a/misc/qet-mcp/test_qet_mcp.py b/misc/qet-mcp/test_qet_mcp.py index 0f7871168..2d96cafe8 100644 --- a/misc/qet-mcp/test_qet_mcp.py +++ b/misc/qet-mcp/test_qet_mcp.py @@ -22,6 +22,7 @@ away: a check that has never been seen to fail is not evidence. from __future__ import annotations +import csv import json import os import re @@ -3675,6 +3676,44 @@ class Integration(unittest.TestCase): note = r["operations"][1].get("note", "") self.assertIn("from_terminal: no terminal", note) self.assertIn("to_terminal: no terminal", note) + def test_wiring_export_names_unnamed_terminals(self): + """--export-wiring gives each end the index add_conductor took and + the uuid the file names the terminal by, even where the terminal + has no name. This strip's file lists its terminals in another order + than the index (top to bottom, then left to right), so a column + holding the file's order would fail here.""" + strip = ("common://10_electric/10_allpole/130_terminals_terminal_strips/" + "90_terminal_strips_diagram/90-10-0211.elmt") + base = self.sb.new() + r = self.ok(self.sb.edit(base, [ + {"op": "add_element", "id": "a", "folio": 0, "path": strip, "x": 100, "y": 200}, + {"op": "add_element", "id": "b", "folio": 0, "path": strip, "x": 400, "y": 200}, + {"op": "add_conductor", "folio": 0, "from": "$a", "from_terminal": 2, + "to": "$b", "to_terminal": 3}, + {"op": "add_conductor", "folio": 0, "from": "$a", "from_terminal": 1, + "to": "$b", "to_terminal": 0}])) + a_uuid, b_uuid = (o["result"] for o in r["operations"][:2]) + out = self.sb.p("wiring.csv") + e = m.tool_export(BINARY, r["output"], "wiring", out) + self.assertTrue(e["ok"], e) + with open(out, encoding="utf-8-sig", newline="") as fh: + rows = list(csv.DictReader(fh, delimiter=";")) + self.assertEqual(len(rows), 2) + # the conductor in the file: which uuid it names each end by + in_file = {c.get("uuid"): {(c.get("element1"), c.get("terminal1")), + (c.get("element2"), c.get("terminal2"))} + for c in ET.parse(r["output"]).getroot().iter("conductor") + if c.get("element1")} + got = set() + for row in rows: + self.assertEqual((row["from_terminal"], row["to_terminal"]), ("", "")) + ends = {(int(row["from_terminal_index"]), row["from_terminal_uuid"]), + (int(row["to_terminal_index"]), row["to_terminal_uuid"])} + self.assertEqual({u for _, u in ends}, + {t for _, t in in_file[row["conductor_uuid"]]}) + got.add(frozenset(i for i, _ in ends)) + self.assertEqual(got, {frozenset({2, 3}), frozenset({1, 0})}) + def test_folio_ref_survives_an_insert_before_it(self): """add_folio names a folio "$f"; inserting another at position 0 moves it from index 1 to 2. "$f" must still name it.""" diff --git a/sources/cli_export.cpp b/sources/cli_export.cpp index c8734cac8..d1e22412d 100644 --- a/sources/cli_export.cpp +++ b/sources/cli_export.cpp @@ -620,6 +620,12 @@ QHash folioIndex(QETProject &project) /// From-to wiring list: one row per conductor, each endpoint resolved to its /// element label and terminal name. /// +/// Most symbols leave their terminals unnamed, so each end also carries the +/// terminal's index -- the one the scripting API's addConductor() takes, +/// empty for two terminals at one point -- and its uuid, the one the project +/// file names the terminal by. These come last, after the columns the list +/// has always had, so a reader of those is not disturbed. +/// /// Reads wiring_list_view out of the project database. --export-cables produces /// the same logical list from the document XML instead, and the two are meant /// to agree: running both and diffing them is a direct check that the database @@ -632,7 +638,8 @@ int exportWiring(QETProject &project, const QString &output) static const QStringList columns { "wire_number", "from_element_label", "from_terminal", - "to_element_label", "to_terminal", "diagram_position", "conductor_uuid" + "to_element_label", "to_terminal", "diagram_position", "conductor_uuid", + "from_terminal_index", "from_terminal_uuid", "to_terminal_index", "to_terminal_uuid" }; QSqlQuery query = project.dataBase()->newQuery( diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index bea454bd4..653bcf172 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -180,8 +180,37 @@ struct DocumentTerminal QString uuid; QString name; bool master_label = false; + QVariant index; }; + //The index each of @p points has in Element::terminals() -- the index + //the scripting API's addConductor() and conductor calls take -- or a + //null QVariant for a point another terminal shares. Element:: + //parseTerminal() sorts the list top to bottom, then left to right, on + //each terminal's position in its definition, and the sort is not + //stable, so which of two terminals at the same point comes first is + //not defined: no index is given rather than one that can change. +QList terminalIndexes(const QList &points) +{ + QList order; + for (int i = 0 ; i < points.size() ; ++i) { + order << i; + } + std::stable_sort(order.begin(), order.end(), [&points](int a, int b) { + if (points.at(a).y() == points.at(b).y()) { + return points.at(a).x() < points.at(b).x(); + } + return points.at(a).y() < points.at(b).y(); + }); + QList indexes(points.size()); + for (int i = 0 ; i < order.size() ; ++i) { + if (points.count(points.at(order.at(i))) == 1) { + indexes[order.at(i)] = i; + } + } + return indexes; +} + struct DocumentDefinition { QString type; @@ -391,6 +420,25 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString data.fromXml(definition); read.type = data.typeToString(); read.sub_type = data.masterTypeToString(); + //Every terminal the element is built with counts towards + //the indexes, as Element::parseTerminal() keeps every one + //whose position reads, in every . + QList parsed; + QList points; + for (QDomElement d = definition.firstChildElement(QStringLiteral("description")) ; + !d.isNull() ; d = d.nextSiblingElement(QStringLiteral("description"))) { + for (QDomElement t = d.firstChildElement(QStringLiteral("terminal")) ; + !t.isNull() ; t = t.nextSiblingElement(QStringLiteral("terminal"))) + { + qreal x, y; + if (QET::attributeIsAReal(t, QStringLiteral("x"), &x) + && QET::attributeIsAReal(t, QStringLiteral("y"), &y)) { + parsed << t; + points << QPointF(x, y); + } + } + } + const QList indexes = terminalIndexes(points); const QDomElement description = definition.firstChildElement(QStringLiteral("description")); for (QDomElement t = description.firstChildElement(QStringLiteral("terminal")) ; !t.isNull() ; t = t.nextSiblingElement(QStringLiteral("terminal"))) @@ -403,6 +451,10 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString terminal.uuid = terminal_uuid.toString(); terminal.name = t.attribute(QStringLiteral("name")); terminal.master_label = t.attribute(QStringLiteral("use_master_label")) == QLatin1String("true"); + const int place = parsed.indexOf(t); + if (place >= 0) { + terminal.index = indexes.at(place); + } read.terminals.insert(terminal_uuid, terminal); } known = definitions.insert(type, read); @@ -582,8 +634,8 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString for (const auto &end : {std::make_pair(conductor.element1, conductor.terminal1), std::make_pair(conductor.element2, conductor.terminal2)}) { const DocumentElement &owner = elements.at(element_index.value(end.first)); - insertTerminal(end.second, end.first, - owner.terminals.value(QUuid(end.second)).name); + const DocumentTerminal &terminal = owner.terminals.value(QUuid(end.second)); + insertTerminal(end.second, end.first, terminal.name, terminal.index); } m_insert_conductor_query.bindValue(QStringLiteral(":uuid"), conductor.uuid); m_insert_conductor_query.bindValue(QStringLiteral(":diagram_uuid"), conductor.diagram_uuid); @@ -1560,6 +1612,7 @@ bool projectDataBase::createDataBase() "uuid VARCHAR(50) NOT NULL, " "element_uuid VARCHAR(50) NOT NULL," "name VARCHAR(50)," + "terminal_index INTEGER," "PRIMARY KEY (uuid, element_uuid)," "FOREIGN KEY (element_uuid) REFERENCES element (uuid)" ")"); @@ -1812,7 +1865,11 @@ void projectDataBase::createWiringListView() "t2.element_uuid AS to_element_uuid," "ei2.label AS to_element_label," "t2.name AS to_terminal," - "d.pos AS diagram_position" + "d.pos AS diagram_position," + "t1.uuid AS from_terminal_uuid," + "t1.terminal_index AS from_terminal_index," + "t2.uuid AS to_terminal_uuid," + "t2.terminal_index AS to_terminal_index" " FROM conductor c" " JOIN terminal t1 ON c.terminal1_uuid = t1.uuid AND c.terminal1_element_uuid = t1.element_uuid" " JOIN terminal t2 ON c.terminal2_uuid = t2.uuid AND c.terminal2_element_uuid = t2.element_uuid" @@ -1999,9 +2056,15 @@ void projectDataBase::populateConductorTable() */ void projectDataBase::insertTerminal(Terminal *terminal) { + const QList terminals = terminal->parentElement()->terminals(); + QList points; + for (const Terminal *t : terminals) { + points << terminal->parentElement()->mapFromScene(t->dockConductor()); + } insertTerminal(terminal->stableUuid().toString(), terminal->parentElement()->uuid().toString(), - terminal->name()); + terminal->name(), + terminalIndexes(points).value(terminals.indexOf(terminal))); } /** @@ -2009,11 +2072,12 @@ void projectDataBase::insertTerminal(Terminal *terminal) insertTerminal(Terminal *) from values rather than a live terminal. */ void projectDataBase::insertTerminal(const QString &uuid, const QString &element_uuid, - const QString &name) + const QString &name, const QVariant &index) { m_insert_terminal_query.bindValue(":uuid", uuid); m_insert_terminal_query.bindValue(":element_uuid", element_uuid); m_insert_terminal_query.bindValue(":name", name); + m_insert_terminal_query.bindValue(":terminal_index", index); if (!m_insert_terminal_query.exec()) { qDebug() << "projectDataBase::insertTerminal insert error : " << m_insert_terminal_query.lastError(); } @@ -2146,7 +2210,7 @@ void projectDataBase::prepareQuery() //INSERT TERMINAL m_insert_terminal_query = QSqlQuery(m_data_base); - m_insert_terminal_query.prepare("INSERT OR IGNORE INTO terminal (uuid, element_uuid, name) VALUES (:uuid, :element_uuid, :name)"); + m_insert_terminal_query.prepare("INSERT OR IGNORE INTO terminal (uuid, element_uuid, name, terminal_index) VALUES (:uuid, :element_uuid, :name, :terminal_index)"); //INSERT CONDUCTOR m_insert_conductor_query = QSqlQuery(m_data_base); diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index 67472af91..1029fcc3d 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -150,7 +150,7 @@ class projectDataBase : public QObject void watchConductor(Conductor *conductor); void insertTerminal(Terminal *terminal); void insertTerminal(const QString &uuid, const QString &element_uuid, - const QString &name); + const QString &name, const QVariant &index); void prepareQuery(); static QHash elementInfoToString( Element *elmt);