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);