diff --git a/sources/qetgraphicsitem/conductor.cpp b/sources/qetgraphicsitem/conductor.cpp index 4d658129f..5c6daa74a 100644 --- a/sources/qetgraphicsitem/conductor.cpp +++ b/sources/qetgraphicsitem/conductor.cpp @@ -2305,6 +2305,38 @@ QPointF Conductor::movePointIntoPolygon(const QPointF &point, const QPainterPath } } +namespace { + /// The two ends of @p conductor on the folio, smaller first, compared + /// by x then y. + QPair conductorEnds(const Conductor *conductor) + { + auto less = [](const QPointF &a, const QPointF &b) { + return a.x() < b.x() || (a.x() == b.x() && a.y() < b.y()); + }; + QPointF a = conductor->terminal1 ? conductor->terminal1->dockConductor() : QPointF(); + QPointF b = conductor->terminal2 ? conductor->terminal2->dockConductor() : QPointF(); + if (less(b, a)) + std::swap(a, b); + return qMakePair(a, b); + } + + /// True if @p a comes before @p b in an order that is the same in every + /// run: by where the conductor's ends are on the folio, then by uuid. + /// Not by uuid first: a project generated again by a script gets new + /// uuids each time, and a file with no conductor uuids gets new ones on + /// every load, while the drawing is the same. + bool stableConductorLess(const Conductor *a, const Conductor *b) + { + const auto ea = conductorEnds(a), eb = conductorEnds(b); + const qreal ka[4] = {ea.first.x(), ea.first.y(), ea.second.x(), ea.second.y()}; + const qreal kb[4] = {eb.first.x(), eb.first.y(), eb.second.x(), eb.second.y()}; + for (int i = 0; i < 4; ++i) + if (ka[i] != kb[i]) + return ka[i] < kb[i]; + return a->uuid() < b->uuid(); + } +} + /** @brief longestConductorInPotential @param conductor : a conductor in the potential to search @@ -2313,10 +2345,24 @@ QPointF Conductor::movePointIntoPolygon(const QPointF &point, const QPainterPath */ Conductor * longestConductorInPotential(Conductor *conductor, bool all_diagram) { Conductor *longest_conductor = conductor; - //Search the longest conductor + qreal longest_length = conductor->length(); + //Search the longest conductor. + //Conductors of equal length are common (a symmetrical layout), and the + //set is iterated in pointer order, which changes from run to run: so a + //tie is broken by an order that does not, or the potential's text lands + //on a different conductor each time the same file is opened or exported. foreach (Conductor *c, conductor -> relatedPotentialConductors(all_diagram)) - if (c -> length() > longest_conductor -> length()) + { + const qreal length = c->length(); + if (qAbs(length - longest_length) < 1e-6) { + if (stableConductorLess(c, longest_conductor)) + longest_conductor = c; + } + else if (length > longest_length) { longest_conductor = c; + longest_length = length; + } + } return longest_conductor; } diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 4aa82faed..45bc2618a 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -395,6 +395,19 @@ target_compile_definitions(tst_wiringlistexport PRIVATE "QET_TEST_BINARY_PATH=\"$\"" "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") +# One text per potential: the number goes on the same conductor in every +# run when two conductors tie for longest. Exports +# fixtures/one_text_per_potential_tie.qet through the real binary, several +# times, since the order it used to depend on changes only between runs. +add_executable( + tst_potentialtextcarrier + tst_potentialtextcarrier.cpp) +add_test(NAME tst_potentialtextcarrier COMMAND tst_potentialtextcarrier) +add_dependencies(tst_potentialtextcarrier qelectrotech) +target_link_libraries(tst_potentialtextcarrier PRIVATE Qt::Test) +target_compile_definitions(tst_potentialtextcarrier PRIVATE + "QET_TEST_BINARY_PATH=\"$\"") + # Hops where wires cross (issue #436): the geometry in wirehops.cpp, and # through the real binary, that --resave keeps the project setting and # adds nothing to a project without it. diff --git a/tests/qttest/fixtures/one_text_per_potential_tie.qet b/tests/qttest/fixtures/one_text_per_potential_tie.qet new file mode 100644 index 000000000..31f27e377 --- /dev/null +++ b/tests/qttest/fixtures/one_text_per_potential_tie.qet @@ -0,0 +1,320 @@ + + + 10/2/26 + 02-10-2026 + 2026-10-02 + one_text_per_potential_tie + one_text_per_potential_tie.qet + 22:07 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + label + + + + + + + + + + + + + + label + + + + + + + + + + + + + + label + + + + + + + + + + + + + + label + + + + + + + + + + + + + + + + + + + + + Elements importats + Zavedené prvky + Importerede elementer + Importierte Elemente + Εισηγμένα στοιχεία + Imported elements + Elementos importados + Éléments importés + Uvezeni elementi + Elementi importati + インバートされた要素 + 전기 심볼 요소 + 전기 심볼 요소 + Elementen geïmporteerd + Elementen geïmporteerd + Elementy importowane + Elementos importados + Elementos importados + Elemente importate + Импортированные элементы + Uvoženi elementi + Importerade element + İthal öğeler + Імпортовані елементи + 导入元件 + + + + كهربائي + 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_全极 + + + + إلكترونيات وأشباه موصلات + Electrònica i semiconductors + Elektronika a polovodiče + Elektronik og halvledere + Elektronik und Halbleiter + Ηλεκτρονικά και ημιαγωγοί + Electronics and semiconductors + Electrónica y semiconductores + Elektroniikka ja puolijohteet + Electronique et semi-conducteurs + Elektronika i poluvodiči + Elektronika és félvezetők + Elettronica e semiconduttori + 電子と半導体 + 전자 공학 및 반도체 + Электроник ба хагас дамжуулагч + Elektronikk og halvledere + Electronica en halfgeleiders + Elektronica en halfgeleiders + Elektronika i półprzewodniki + Eletrónica e semicondutores + Eletrônica e semi-condutoues + Electronică și semiconductoare + Электроника и полупроводники + Elektronika a polovodiče + Elektronika in polprevodniki + Електроника и полупроводници + Elektronik och halvledare + Elektronik ve yarı iletkenler + Електроніка та напівпровідники + 电子与半导体 + + + + مُقاومة + Resistències + Odpory + Modstande + Widerstände + Αντιστάσεις + Resistors + Resistencias + Vastukset + Résistances + Otpornici + Ellenállások + Resistori + 抵抗器 + 저항 + Эсэргүүцэл + Motstander + Weerstand + Weerstanden + Rezystory + Resistência + Resistores + Rezistoare + Резисторы + Rezistory + Upori + Отпорници + Motstånd + Dirençler + Резистори + 电阻器 + + + + + + مُقاومة + Resistència + Odpor + Modstand + Widerstand + Αντίσταση + Resistor + Resistencia + Vastus + Résistance + Otpornik + Ellenállás + Resistore + 抵抗器 + 저항 + Резистор + Motstand + Weerstand + Weerstand + Rezystor + Resistência + Resistor + Rezistor + Резистор + Rezistor + Upor + Отпорник + Resistor + Direnç + Резистор + 电阻 + + EN 60617: 04-01-01 + + + + + + label + + + + + + + + + + + + + diff --git a/tests/qttest/tst_potentialtextcarrier.cpp b/tests/qttest/tst_potentialtextcarrier.cpp new file mode 100644 index 000000000..97d8c28d2 --- /dev/null +++ b/tests/qttest/tst_potentialtextcarrier.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include + +#include +#include +#include +#include +#include +#include + +// With "one text per potential" on, the potential's number is drawn on its +// longest conductor. fixtures/one_text_per_potential_tie.qet has two +// conductors of the same, greatest length in one potential: which of them +// carried the number used to follow pointer order, so the same file +// exported twice could put it on either. Runs the real binary, in separate +// processes, since that order only changes from one run to the next. +class tst_potentialtextcarrier : public QObject +{ + Q_OBJECT + + QTemporaryDir m_dir; + + // Export the fixture to SVG and return where the number is drawn: the + // transform of the group holding it. Empty if it is not drawn exactly + // once. + QString carrier(int run) + { + const QString home = m_dir.filePath(QStringLiteral("home%1").arg(run)); + const QString out = m_dir.filePath(QStringLiteral("svg%1").arg(run)); + QDir().mkpath(home); + 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("/data")); + env.insert(QStringLiteral("TMPDIR"), home); + QProcess proc; + proc.setProcessEnvironment(env); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), + {QStringLiteral("--export-svg"), + QFINDTESTDATA("fixtures/one_text_per_potential_tie.qet"), out}); + if (!proc.waitForFinished(60000) || proc.exitCode() != 0) + return {}; + + const QStringList svgs = QDir(out).entryList({QStringLiteral("*.svg")}); + if (svgs.size() != 1) + return {}; + QFile f(QDir(out).filePath(svgs.first())); + if (!f.open(QIODevice::ReadOnly)) + return {}; + const QString svg = QString::fromUtf8(f.readAll()); + + static const QRegularExpression re( + QStringLiteral("transform=\"([^\"]*)\"[^<]*>\\s*]*>42")); + QStringList found; + auto it = re.globalMatch(svg); + while (it.hasNext()) + found << it.next().captured(1); + return found.size() == 1 ? found.first() : QString(); + } + +private slots: + void initTestCase() + { + QVERIFY(m_dir.isValid()); + QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH))); + } + + // Before the fix about half the runs differed from the first, so + // eight agreeing runs leave a 1 in 128 chance of a false pass. + void sameCarrierEveryRun() + { + const QString first = carrier(0); + QVERIFY2(!first.isEmpty(), "the number is not drawn exactly once"); + for (int run = 1; run < 8; ++run) + QCOMPARE(carrier(run), first); + } +}; + +QTEST_GUILESS_MAIN(tst_potentialtextcarrier) +#include "tst_potentialtextcarrier.moc"