From 972127aa05e4b2d89274808c97d1865bf8c9d0ed Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 30 Sep 2026 22:59:49 +1300 Subject: [PATCH] Wiring list export: keep every wire at a folio report, print the folio number A wire drawn to a folio report waited, keyed by its report, for the wire on the linked report, in a QMap whose insert() replaces: a second wire on the same report pushed the first out of the list, and a pair with several wires on a side was joined by whichever came first. On the shipped examples, re-saved as the export reads them, 130 of 3,189 wires were missing from the list (industrial 86, m_000 41, affuteuse_250h 3). All the report halves are now collected first, and two are joined only when each report of a linked pair has exactly one wire. With several on a side the diagram does not say which terminal is wired to which, so each wire gets its own row ending at the report, as an unmatched half already did. The result no longer depends on the order the folios are saved in. The Page column printed each folio's number template ("%id/%total") and the rows were sorted by it. It now prints the number the folio shows, and the rows are sorted by the folio's position in the project. tst_wiringlistexport checks a fixture with one report pair of each kind, and the page column on every example. Co-Authored-By: Claude Opus 5.5 --- sources/wiringlistexport.cpp | 120 ++- sources/wiringlistexport.h | 1 + tests/qttest/CMakeLists.txt | 14 + tests/qttest/fixtures/wiring_list_arrows.qet | 871 +++++++++++++++++++ tests/qttest/tst_wiringlistexport.cpp | 112 +++ 5 files changed, 1056 insertions(+), 62 deletions(-) create mode 100644 tests/qttest/fixtures/wiring_list_arrows.qet create mode 100644 tests/qttest/tst_wiringlistexport.cpp diff --git a/sources/wiringlistexport.cpp b/sources/wiringlistexport.cpp index e688a1460..df81bf25c 100644 --- a/sources/wiringlistexport.cpp +++ b/sources/wiringlistexport.cpp @@ -1,5 +1,6 @@ #include "wiringlistexport.h" #include "qetproject.h" +#include "diagram.h" #include #include #include @@ -24,6 +25,13 @@ QString WiringListExport::normalizeUuid(const QString &u) const QString WiringListExport::findDiagramFolio(const QDomElement &diagramElem) const { if (diagramElem.isNull()) return ""; + // The folio number as the folio shows it ("3/12"). The saved "folio" + // attribute is its template ("%id/%total"). + const int index = diagramElem.attribute("order").toInt(); + if (m_project && index >= 1 && index <= m_project->diagrams().size()) { + return m_project->diagrams().at(index - 1)->border_and_titleblock + .titleblockInformation().value("folio").toString(); + } if (diagramElem.hasAttribute("folio")) return diagramElem.attribute("folio"); if (diagramElem.hasAttribute("title")) return diagramElem.attribute("title"); return ""; @@ -140,6 +148,7 @@ QList WiringListExport::collectConductors(const QDomElement &root QDomElement diag = climbToDiagram(cond); data.folio = findDiagramFolio(diag); + data.folio_index = diag.attribute("order").toInt(); if (data.folio.isEmpty()) data.folio = cond.attribute("folio", cond.attribute("page", "")); conductors.append(data); @@ -227,7 +236,10 @@ QString WiringListExport::toCsvString() const QList conductors = collectConductors(doc.documentElement()); QList uniqueConductors; - QMap partialWires; + // Conductors with one end on a folio report, by that report. All of them + // are collected before any is merged, so the result does not depend on + // the order the folios are saved in. + QMap> partialWires; auto normalizePartial = [](ConductorData c, const QString &ph_uuid) { if (c.el1_uuid == ph_uuid) { @@ -277,40 +289,50 @@ QString WiringListExport::toCsvString() const } QString ph_uuid = el1_ph ? c.el1_uuid : c.el2_uuid; - ConductorData normC = normalizePartial(c, ph_uuid); - - QString matching_ph_uuid; - if (!elementsInfo[ph_uuid].links.isEmpty()) { - matching_ph_uuid = elementsInfo[ph_uuid].links.first(); - } - - if (!matching_ph_uuid.isEmpty() && partialWires.contains(matching_ph_uuid)) { - ConductorData otherHalf = partialWires.take(matching_ph_uuid); - - ConductorData merged; - merged.folio = mergeField(otherHalf.folio, normC.folio); - - merged.el1_uuid = otherHalf.el1_uuid; - merged.element1_label = otherHalf.element1_label; - merged.terminalname1 = otherHalf.terminalname1; - - merged.el2_uuid = normC.el1_uuid; - merged.element2_label = normC.element1_label; - merged.terminalname2 = normC.terminalname1; - - merged.tension_protocol = mergeField(otherHalf.tension_protocol, normC.tension_protocol); - merged.conductor_color = mergeField(otherHalf.conductor_color, normC.conductor_color); - merged.conductor_section = mergeField(otherHalf.conductor_section, normC.conductor_section); - merged.function = mergeField(otherHalf.function, normC.function); - - uniqueConductors.append(merged); - } else { - partialWires.insert(ph_uuid, normC); - } + partialWires[ph_uuid].append(normalizePartial(c, ph_uuid)); } - for (const ConductorData &leftover : partialWires.values()) { - uniqueConductors.append(leftover); + // Two halves are one wire only when each report of a linked pair carries + // exactly one conductor. With several on a side, the diagram does not say + // which terminal is wired to which, so each conductor gets its own row, + // ending at the report. + QSet written; + for (auto it = partialWires.cbegin(); it != partialWires.cend(); ++it) { + const QString &ph_uuid = it.key(); + if (written.contains(ph_uuid)) continue; + written.insert(ph_uuid); + + const QStringList links = elementsInfo.value(ph_uuid).links; + const QString matching_ph_uuid = links.isEmpty() ? QString() : links.first(); + const QList others = partialWires.value(matching_ph_uuid); + + if (it.value().size() != 1 || others.size() != 1 || written.contains(matching_ph_uuid)) { + uniqueConductors.append(it.value()); + continue; + } + written.insert(matching_ph_uuid); + + const ConductorData &otherHalf = it.value().first(); + const ConductorData &normC = others.first(); + + ConductorData merged; + merged.folio = mergeField(otherHalf.folio, normC.folio); + merged.folio_index = std::min(otherHalf.folio_index, normC.folio_index); + + merged.el1_uuid = otherHalf.el1_uuid; + merged.element1_label = otherHalf.element1_label; + merged.terminalname1 = otherHalf.terminalname1; + + merged.el2_uuid = normC.el1_uuid; + merged.element2_label = normC.element1_label; + merged.terminalname2 = normC.terminalname1; + + merged.tension_protocol = mergeField(otherHalf.tension_protocol, normC.tension_protocol); + merged.conductor_color = mergeField(otherHalf.conductor_color, normC.conductor_color); + merged.conductor_section = mergeField(otherHalf.conductor_section, normC.conductor_section); + merged.function = mergeField(otherHalf.function, normC.function); + + uniqueConductors.append(merged); } for (ConductorData &c : uniqueConductors) { @@ -322,35 +344,9 @@ QString WiringListExport::toCsvString() const } std::sort(uniqueConductors.begin(), uniqueConductors.end(), [](const ConductorData &a, const ConductorData &b) { - QStringList partsA = a.folio.split(','); - QStringList partsB = b.folio.split(','); - int minLen = std::min(partsA.size(), partsB.size()); - int folioCmp = 0; - - for (int i = 0; i < minLen; ++i) { - bool okA, okB; - int numA = partsA[i].trimmed().toInt(&okA); - int numB = partsB[i].trimmed().toInt(&okB); - - if (okA && okB) { - if (numA != numB) { - folioCmp = (numA < numB) ? -1 : 1; - break; - } - } else { - int strCmp = partsA[i].trimmed().compare(partsB[i].trimmed(), Qt::CaseInsensitive); - if (strCmp != 0) { - folioCmp = strCmp; - break; - } - } - } - - if (folioCmp == 0 && partsA.size() != partsB.size()) { - folioCmp = (partsA.size() < partsB.size()) ? -1 : 1; - } - - if (folioCmp != 0) return folioCmp < 0; + // By the folio's position in the project: its number is free text + // ("3/12", "A-2") and does not sort. + if (a.folio_index != b.folio_index) return a.folio_index < b.folio_index; int el1Cmp = a.element1_label.toLower().compare(b.element1_label.toLower()); if (el1Cmp != 0) return el1Cmp < 0; diff --git a/sources/wiringlistexport.h b/sources/wiringlistexport.h index b61779267..741a81dd4 100644 --- a/sources/wiringlistexport.h +++ b/sources/wiringlistexport.h @@ -33,6 +33,7 @@ struct ConductorData { QString conductor_section; QString function; QString folio; + int folio_index = 0; // position of the folio in the project, from 1 }; /** diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 1371aa6d6..97d85189a 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -362,6 +362,20 @@ target_link_libraries(tst_conductorselfretrace PRIVATE Qt::Test) target_compile_definitions(tst_conductorselfretrace PRIVATE "QET_TEST_BINARY_PATH=\"$\"") +# The wiring list export (--export-cables) keeps every wire drawn to a folio +# report when a report has several, joins only one-to-one report pairs, and +# prints the folio number rather than its template. fixtures/wiring_list_arrows.qet +# has one of each report pair; the examples check the page column. +add_executable( + tst_wiringlistexport + tst_wiringlistexport.cpp) +add_test(NAME tst_wiringlistexport COMMAND tst_wiringlistexport) +add_dependencies(tst_wiringlistexport qelectrotech) +target_link_libraries(tst_wiringlistexport PRIVATE Qt::Test) +target_compile_definitions(tst_wiringlistexport PRIVATE + "QET_TEST_BINARY_PATH=\"$\"" + "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") + # qet.conductorUuids() / qet.conductorEnds(): a script lists a folio's # conductors by uuid and finds each one's two ends. Runs a script through # the real binary's --run on fixtures/qet_bug_repro_resaved.qet, so only diff --git a/tests/qttest/fixtures/wiring_list_arrows.qet b/tests/qttest/fixtures/wiring_list_arrows.qet new file mode 100644 index 000000000..0ab8a0eff --- /dev/null +++ b/tests/qttest/fixtures/wiring_list_arrows.qet @@ -0,0 +1,871 @@ + + + + ENA006 SLD probe title block � generated. + + + + + Z + + + + + + 2026-08-11 + + + + + E-110: G4 CLOSED - LEVITON B3277-YB SIDE-MOUNT SPD + 3P 20 A CB-SPD + + + + + CC + + + + + + PRELIMINARY + + + + + NOT FOR CONSTRUCTION + + + + + Y + + + + + + 2026-08-11 + + + + + BREAKER AUX/POSITION DELETED - s60 GOVERNS; G6 CLOSED-DEL; A-5 RETIRED + + + + + CC + + + + + + + + + + + + X + + + + + + 2026-08-11 + + + + + COUNCIL REVIEW - WRAP/REFS/T3/TEE; KEY-2 PAWL; G14 ACCESS; H-OTD + + + + + CC + + + + + + + + + + + + MEDIUM VOLTAGE POWER ELECTRONICS LABORATORY � UNIVERSITY OF CALGARY, SCHULICH SCHOOL OF ENGINEERING + + + + + %plant + + + + + + %locmach + + + + + + %indexrev + + + + + + %folio + + + + + + %title + + + + + + %author + + + + + + %date + + + + + + NTS + + + + + + D + + + + + + ENA006 MV TEST FACILITY � ENG A 006 + + + + + + PILOT ARTIFACT + + + + + NOT A DELIVERABLE + + + + + + + 8/12/2026 + 12-08-2026 + 2026-08-12 + qet_bug_repro + C:/Users/phili/.claude/skills/cabinet-drawings/pilot_qet/out/qet_bug_repro.qet + 19:09 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PA + + + + PA + label + + + + + + + + + + + + + + + 2-A1 + label + + + + + + + + + + + + + + + + + + + + + + + + + 1-A6 + label + + + + + + + + + + + + PB + + + + PB + label + + + + + + + + + + + + + + + + + + + + + + EA + + + + EA + label + + + + + + + + + + + + EA2 + + + + EA2 + label + + + + + + + + + + + + + + + 4-A1 + label + + + + + + + + + + + + + + + + + + + + + + + + + + + + 3-A6 + label + + + + + + + + + + + + EB + + + + EB + label + + + + + + + + + + + + + + + + + + + + + + LA + + + + LA + label + + + + + + + + + + + + + + + 6-A1 + label + + + + + + + + + + + + + + + + + + + + + + + + + 5-A6 + label + + + + + + + + + + + + LB + + + + LB + label + + + + + + + + + + + + LD + + + + LD + label + + + + + + + + + + + + + + + + + + + كهربائي + Elèctric + Elektrotechnika + Elektrisk + Elektrik + Ηλεκτρικά + Electric + Eléctrica + Sähkö + Electrique + Električno + Elektromos + Elettrica + 電気 + 전기 + Цахилгаан + Elektrisk + Elektrotechniek + Elektrisch + Elektrotechnika + Elétrico + Elétrica + Electric + Электротехника + Elektrické + Elektrika + Електрика + El + Elektrik + Електрика + 10_电气 + 电气工程 + + + + متعدد الأقطاب + Multifilar + Vícežilový + Flere ledere + Allpolig + Πολυγραμμικό + All-pole + Multifilar + Yleisnapa + Multifilaire + Univerzalni + Többvonalas szimbólumok + Multifilare + 複線 + 다-극 + Бүх туйл + Alle poler + Veel polig + All-pole + Schematy wieloliniowe + Multipolar + Multifilar + Bipolar + Многополюсные + Všepólové + Vsi poli + Свe-полни + Allpolig + Çok kutuplu + Багатополюсні + 10_全极 + + + + إحالات + Referències als fulls + Odkazování listů + Ark henvisninger + Folienverweise + Αναφορές σελίδας + Folio referencing + Referencias + Lehtiviittaus + Reports de folio + Referenciranje folija + Tervlap hivatkozások + Riferimenti + フォリオ参照 + 도면 간 참조 + Хуудасны холбоос + Folio-referanse + Referenties + Folioverwijzing + Odsyłacze + Renvoi de folio + Referências + Renvoi de folio + Точки разрыва + Odkazy na listy + Sklici na list + Референцирање фолија + Bladhänvisning + Sayfa referansı + Посилання на аркуші + 图页引用 + + + + + + الصفحة التالية + Enllaç sortint + Další list + Går pil + Nächste Folie + Επόμενη σελίδα + Going arrow + Folio siguiente + Lähtevä nuoli + Folio suivant + Odlazna strelica + Elmenő hivatkozás + Foglio successivo + 出矢印 + 나가는 화살표 + Явж буй сум + Går pil + Referentie volgend + Gaande pijl + Wyjście adresowe + Seta de saída + Seta de saída (vai) + Săgeată de ieșire + Следующая страница + Odchádzajúca šípka + Odhodna puščica + Одлазна стрелица + Utgående pil + Giden ok + Стрілка виходу + 02引出箭头 + + Author: The QElectroTech team +License: see http://qelectrotech.org/wiki/doc/elements_license + + + + + + label + + + + + + + + + + الصفحة السابقة + Enllaç entrant + Předchozí list + Kommer pil + Vorherige Folie + Προηγούμενη σελίδα + Coming arrow + folio anterior + Saapuva nuoli + Folio précédent + Dolazna strelica + Beérkező hivatkozás + Foglio precedente + 入り矢印 + 들어오는 화살표 + Ирж буй сум + Kommer pil + Referentie Vorig + Komende pijl + Wejście adresowe + Seta de entrada + Seta de entrada (vem) + Săgeată de intrare + Предыдущая страница + Prichádzajúca šípka + Prihajajoča puščica + Долазна стрелица + Inkommande pil + Gelen ok + Стрілка входу + 01引入箭头 + + Author: The QElectroTech team +License: see http://qelectrotech.org/wiki/doc/elements_license + + + + + + label + + + + + + + + + الإشارات والتشغيل + Senyalització i commandament + Značení a příkazy + Signalering og betjening + Melde- und Bedieneinrichtungen + Σήμανση και χειρισμός + Signaling and operating + Señalización y comando + Merkinnät ja käyttö + Signalétique et commande + Signalizacija i upravljanje + Jelzés és kezelés + Segnalazione e comando + 信号と操作 + 신호 및 작동 + Дохиолол ба удирдлага + Signalering og betjening + Signalering en bediening + Signalering en bediening + Obwody sterownicze + Sinalização e comando + Sinalização e Comando + Semnalizare și operare + Сигнализация и управление + Signalizácia a ovládanie + Signalizacija in upravljanje + Сигнализација и управљање + Signalering och manövrering + Sinyalizasyon ve kumanda + Сигналізація та керування + 信号与操作 + + + + أجهزة إشارة صوتية + Dispositius de senyalització acústica + Akustická signalizace + Akustisk signaludstyr + Akustische Meldeeinrichtungen + Συσκευές ηχητικής σήμανσης + Acoustic signaling devices + Dispositivos de señalamiento acustico + Akustiset merkinantolaitteet + Signalements sonores + Akustični signalni uređaji + Hangjelző eszközök + Segnalazione acustica + 音響信号装置 + 음향 신호 장치 + Дуут дохиоллын төхөөрөмж + Akustiske signalenheter + Akoestische signaaltoestellen + Akoestische signaaltoestellen + Sygnalizatory dźwiękowe + Dispositivos de sinalização acústica + Dispositivos de sinalizacao acustica + Dispozitive de semnalizare acustică + Звуковая сигнализация + Akustické signalizačné zariadenia + Zvočne signalne naprave + Звучни сигнални уређаји + Akustiska signaldon + Akustik sinyalizasyon cihazları + Акустичні сигнальні пристрої + 12_声音信号 + + + + + + صفّارة إنذار + Sirena + Siréna + Sirene + Sirene + Σειρήνα + Siren + Sirena + Sireeni + Sirène + Sirena + Sziréna + Sirena + サイレン + 사이렌 + Сирена + Sirene + Sirene + Sirene + Syrena + Sirene + Sirene + Sirenă + Сирена + Siréna + Sirena + Сирена + Siren + Siren + Сирена + 警笛 + + Author: The QElectroTech team +License: see http://qelectrotech.org/wiki/doc/elements_license + + + + label + + + + + + + + + + + + + + + diff --git a/tests/qttest/tst_wiringlistexport.cpp b/tests/qttest/tst_wiringlistexport.cpp new file mode 100644 index 000000000..399ac58af --- /dev/null +++ b/tests/qttest/tst_wiringlistexport.cpp @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include + +#include +#include +#include +#include +#include + +// The wiring list (--export-cables, and the wiring plan CSV export of the +// GUI) joins the two halves of a wire drawn to a folio report. The fixture +// has three pairs of folios, each joined by a linked going/coming report: +// P: one wire on each side -> one row, both folios +// E: two wires on the first side, one -> three rows, none dropped +// L: one wire, two on the second side -> three rows +// With several wires on a side the diagram does not say which terminal is +// wired to which, so those wires are not joined. The Page column is the +// folio number as the folio shows it, not its "%id/%total" template, and +// rows come in folio order. +class tst_wiringlistexport : public QObject +{ + Q_OBJECT + + QTemporaryDir m_dir; + int m_run = 0; + + // The rows of the wiring list, header left out. Sets @p ok to whether + // the export ran, so a project with no wires is not mistaken for a + // failed run. + QStringList exportCables(const QString &project, bool *ok) + { + *ok = false; + const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run)); + const QString tmp = m_dir.filePath(QStringLiteral("tmp%1").arg(m_run)); + const QString out = m_dir.filePath(QStringLiteral("out%1.csv").arg(m_run++)); + QDir().mkpath(home); + QDir().mkpath(tmp); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); + env.insert(QStringLiteral("HOME"), home); + env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/.config")); + env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/.local/share")); + env.insert(QStringLiteral("TMPDIR"), tmp); + + QProcess proc; + proc.setProcessEnvironment(env); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), + {QStringLiteral("--export-cables"), project, out}); + if (!proc.waitForFinished(60000) || proc.exitCode() != 0) + return {}; + + QFile file(out); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + return {}; + *ok = true; + QStringList rows = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n'), + Qt::SkipEmptyParts); + if (!rows.isEmpty()) + rows.removeFirst(); // header, translated + return rows; + } + +private slots: + void initTestCase() + { + QVERIFY(m_dir.isValid()); + QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH))); + } + + void reportPairs() + { + const QString fixture = QFINDTESTDATA("fixtures/wiring_list_arrows.qet"); + QVERIFY2(!fixture.isEmpty(), "fixture project not found"); + + const QStringList expected { + QStringLiteral("1/6, 2/6;PA;A1;PB;A1;;;;"), + QStringLiteral("3/6;EA;A1;;1;;;;"), + QStringLiteral("3/6;EA2;A1;;1;;;;"), + QStringLiteral("4/6;EB;A1;;1;;;;"), + QStringLiteral("5/6;LA;A1;;1;;;;"), + QStringLiteral("6/6;LB;A1;;1;;;;"), + QStringLiteral("6/6;LD;A1;;1;;;;"), + }; + bool ok; + QCOMPARE(exportCables(fixture, &ok), expected); + QVERIFY(ok); + } + + void pageIsTheFolioNumber_data() + { + QTest::addColumn("project"); + const QDir examples(QStringLiteral(QET_EXAMPLES_DIR)); + for (const QString &f : examples.entryList({QStringLiteral("*.qet")}, QDir::Files, QDir::Name)) + QTest::newRow(f.toUtf8().constData()) << examples.filePath(f); + } + + void pageIsTheFolioNumber() + { + QFETCH(QString, project); + bool ok; + const QStringList rows = exportCables(project, &ok); + QVERIFY2(ok, "--export-cables failed"); + for (const QString &row : rows) { + const QString page = row.section(QLatin1Char(';'), 0, 0); + QVERIFY2(!page.contains(QLatin1Char('%')), qPrintable(row)); + } + } +}; + +QTEST_APPLESS_MAIN(tst_wiringlistexport) + +#include "tst_wiringlistexport.moc"