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"