From bec0f1c7b4856aead15d2e39ae64c421090309b5 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 4 Oct 2026 10:57:27 +1300 Subject: [PATCH] Fix #1280: junction dot missing where a wire's corner meets a wire it is not wired to A conductor draws a junction dot at each of its bends that lies on another conductor. It only looked at the conductors on its own two terminals, so a bend lying on a conductor of the same potential that shares no terminal with it got no dot. Dragging the horizontal parts of a chain of conductors onto one line, as in the report, does exactly that. Look at every conductor of the potential on the folio instead (relatedPotentialConductors(false)). The folio scene uses NoIndex, so a scene lookup at each bend scans every item and made export of the 366-conductor Polonez example 60% slower; walking the potential costs nothing measurable there (2.08 s vs 2.11 s) or on a 153-conductor potential (0.97 s both). The test for one other conductor moves into bendMakesJunction(). It also stops a point being appended once per matching segment: master drew the dot above K2 in the fixture ten times. Across the 23 examples (133 folios) exported to SVG, 6 dots are added, all at T-joints that had none, and none removed. tst_junctiondots exports fixtures/junction_dot_shared_potential.qet and checks for both dots; it fails on master with the dot above K3 missing. Co-Authored-By: Claude Opus 5.5 --- sources/qetgraphicsitem/conductor.cpp | 83 ++-- sources/qetgraphicsitem/conductor.h | 1 + tests/qttest/CMakeLists.txt | 12 + .../junction_dot_shared_potential.qet | 354 ++++++++++++++++++ tests/qttest/tst_junctiondots.cpp | 81 ++++ 5 files changed, 501 insertions(+), 30 deletions(-) create mode 100644 tests/qttest/fixtures/junction_dot_shared_potential.qet create mode 100644 tests/qttest/tst_junctiondots.cpp diff --git a/sources/qetgraphicsitem/conductor.cpp b/sources/qetgraphicsitem/conductor.cpp index aab4465d3..c51c4f8ce 100644 --- a/sources/qetgraphicsitem/conductor.cpp +++ b/sources/qetgraphicsitem/conductor.cpp @@ -2107,16 +2107,58 @@ QPainterPath Conductor::paintedPath() const } /** - @return la liste des positions des jonctions avec d'autres conducteurs + @return true if \a scene_point, where this conductor bends with + \a bend_type, lies on one of the segments of \a c and \a c does not + bend the same way at the same point. +*/ +bool Conductor::bendMakesJunction(const Conductor *c, const QPointF &scene_point, + Qt::Corner bend_type) const +{ + // exprime le point dans les coordonnees de l'autre conducteur + const QPointF conductor_point = c -> mapFromScene(scene_point); + bool on_conductor = false; + for (ConductorSegment *segment : c -> segmentsList()) + { + if (isContained(conductor_point, segment -> firstPoint(), segment -> secondPoint())) + { + on_conductor = true; + break; + } + } + if (!on_conductor) + return false; + + // ce point commun ne doit pas etre une bifurcation identique a celle-ci + for (const ConductorBend &cb : c -> bends()) + { + if (cb.first == conductor_point && cb.second == bend_type) + return false; + } + return true; +} + +/** + @return the positions, in this conductor's coordinates, of the junction + dots this conductor draws: each of its bends that lies on another + conductor of the same potential, unless that conductor bends the same + way at the same point. */ QList Conductor::junctions() const { QList junctions_list; - // pour qu'il y ait des jonctions, il doit y avoir d'autres conducteurs et des bifurcations - QList other_conductors = relatedConductors(this); QList bends_list = bends(); - if (other_conductors.isEmpty() || bends_list.isEmpty()) { + if (bends_list.isEmpty()) { + return(junctions_list); + } + + // Every conductor of the potential on this folio, not only those on + // this conductor's own terminals: a bend can lie on a conductor it + // shares no terminal with, e.g. when the horizontal parts of a chain + // of conductors are dragged over each other (issue #1280). + const QList other_conductors = + const_cast(this)->relatedPotentialConductors(false).values(); + if (other_conductors.isEmpty()) { return(junctions_list); } @@ -2139,37 +2181,18 @@ QList Conductor::junctions() const // si le point n'est pas une bifurcation, il ne peut etre une jonction (enfin pas au niveau de ce conducteur) if (!is_bend) continue; - bool is_junction = false; QPointF scene_point = mapToScene(point); - foreach(Conductor *c, other_conductors) + + bool is_junction = false; + for (Conductor *c : other_conductors) { - // exprime le point dans les coordonnees de l'autre conducteur - QPointF conductor_point = c -> mapFromScene(scene_point); - // recupere les segments de l'autre conducteur - QList c_segments = c -> segmentsList(); - if (c_segments.isEmpty()) - continue; - // parcoure les segments a la recherche d'un point commun - for (int j = 0 ; j < c_segments.count() ; ++ j) + if (bendMakesJunction(c, scene_point, current_bend_type)) { - ConductorSegment *segment = c_segments[j]; - // un point commun a ete trouve sur ce segment - if (isContained(conductor_point, segment -> firstPoint(), segment -> secondPoint())) - { - is_junction = true; - // ce point commun ne doit pas etre une bifurcation identique a celle-ci - QList other_conductor_bends = c -> bends(); - foreach(ConductorBend cb, other_conductor_bends) - { - if (cb.first == conductor_point && cb.second == current_bend_type) - { - is_junction = false; - } - } - } - if (is_junction) junctions_list << point; + is_junction = true; + break; } } + if (is_junction) junctions_list << point; } return(junctions_list); } diff --git a/sources/qetgraphicsitem/conductor.h b/sources/qetgraphicsitem/conductor.h index feff48004..387fc9d20 100644 --- a/sources/qetgraphicsitem/conductor.h +++ b/sources/qetgraphicsitem/conductor.h @@ -237,6 +237,7 @@ class Conductor : public QGraphicsObject uint segmentsCount(QET::ConductorSegmentType = QET::Both) const; QList segmentsToPoints() const; QList bends() const; + bool bendMakesJunction(const Conductor *, const QPointF &, Qt::Corner) const; void pointsToSegments(const QList&); Qt::Corner currentPathType() const; diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 20b257ca1..d845df4fd 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -441,6 +441,18 @@ target_link_libraries(tst_potentialtextcarrier PRIVATE Qt::Test) target_compile_definitions(tst_potentialtextcarrier PRIVATE "QET_TEST_BINARY_PATH=\"$\"") +# Junction dots: a wire's corner that lies on another wire of the same +# potential gets a dot, even when the two share no terminal (issue #1280). +# Exports fixtures/junction_dot_shared_potential.qet through the real binary. +add_executable( + tst_junctiondots + tst_junctiondots.cpp) +add_test(NAME tst_junctiondots COMMAND tst_junctiondots) +add_dependencies(tst_junctiondots qelectrotech) +target_link_libraries(tst_junctiondots PRIVATE Qt::Test) +target_compile_definitions(tst_junctiondots PRIVATE + "QET_TEST_BINARY_PATH=\"$\"") + # With SOURCE_DATE_EPOCH set, --export-pdf writes the same bytes for the # same project in every run. Exports examples/741.qet several times, in # separate processes. diff --git a/tests/qttest/fixtures/junction_dot_shared_potential.qet b/tests/qttest/fixtures/junction_dot_shared_potential.qet new file mode 100644 index 000000000..dbe55f8c8 --- /dev/null +++ b/tests/qttest/fixtures/junction_dot_shared_potential.qet @@ -0,0 +1,354 @@ + + + 10/4/26 + 04-10-2026 + 2026-10-04 + skeleton + /tmp/qet-mcp-new-jk9lz64u/skeleton.qet + 10:28 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 14 + + + 11 + + + + label + + + + + + + + + + + + + 14 + + + 11 + + + + label + + + + + + + + + + + + + 14 + + + 11 + + + + label + + + + + + + + + + + + + 14 + + + 11 + + + + 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_全极 + + + + المرحّلات والموصّلات وجهات الاتصال + Relés, contactors i contactes + Relé, stykače a kontakty + Relæer, kontaktorer og kontakter + Relais, Schütze und Kontakte + Ηλεκτρονόμοι και επαφές + Relays, contactors and contacts + Relevadores, contactores y contactos + Releet, kontaktorit ja koskettimet + Relais, contacteurs et contacts + Releji, kontaktori i kontakti + Relék, kontaktorok és érintkezők + Relè, contattori e contatti + 継電器、接触器、接点 + 릴레이, 커넥터및 접점 + Реле, контактор болон контакт + Reléer, kontaktorere og kontakter + Relais, contactoren en contacten + Relais, contacteurs en contacten + Przekaźniki i styczniki + Relés, contatores e contactos + Relés, Contatores e contatos + Relee, contactoare și contacte + Реле, пускатели и контакты + Relé, stykače a kontakty + Releji, kontaktorji in kontakti + Релеји, контактори и контакти + Reläer, kontaktorer och kontakter + Röleler, kontaktörler ve kontaklar + Реле, контактори та контакти + 继电器、接触器与触点 + + + + جهات الاتصال + Contactes + Kontakty + Kontaktpersoner + Kontakte + Επαφές + Contacts + Contactos + Kosketimet + Contacts + Kontakti + Kapcsolatok + Contatti + 連絡先 + 접점 + Харилцагчид + Kontakter + Contacten + Contacten + Łączność + Contatos + Contatos + Contacte + Контакты + Kontakty + Stiki + Контакти + Kontakter + Kişiler + Контакти + 触点 + + + + + + عدّاد مفتوح عادة + Taulell normalment obert + Kontakt čítače (NO) + Normalt åben tæller + Öffner Zähler betätigt + Ανοικτή επαφή μετρητή + Normally open counter + Contador NA + Normaalisti avoin laskuri + Actionné par comptage NO + Brojač normalno otvoren + Alaphelyzetben nyitott számláló + Contatto azionato da un contatore (NO) + 常開カウンター + 평상시 열림 카운터 + Ихэвчлэн нээлттэй тоолуур + Normalt åpen teller + Teller contact (NO) + Normaal open teller + Zestyk zwierny licznika + Contador normalmente aberto + Contato NA acionado por contador + Contor normal deschis + Счётчик с нормально открытым контактом + Počítadlo s kontaktom NO + Števec normalno odprt + Бројач нормално отворен + Räknare med slutande kontakt + Normalde açık sayaç + Лічильник нормально розімкнений + 计数驱动常开触点 + + Author: The QElectroTech team +License: see http://qelectrotech.org/wiki/doc/elements_license + + + + + 14 + + + 11 + + + + label + + + + + + + + + + + + + + + + diff --git a/tests/qttest/tst_junctiondots.cpp b/tests/qttest/tst_junctiondots.cpp new file mode 100644 index 000000000..33000b781 --- /dev/null +++ b/tests/qttest/tst_junctiondots.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include + +#include +#include +#include +#include +#include +#include +#include + +// fixtures/junction_dot_shared_potential.qet: four contacts K1..K4 in a row, +// wired K1-K2, K1-K4 and K2-K3. All three wires run along one line above the +// contacts, so there is a T above K2 and another above K3. The corner above +// K3 belongs to K2-K3 and lies on K1-K4, which shares no terminal with it: +// that dot used to be missing (issue #1280). +class tst_junctiondots : public QObject +{ + Q_OBJECT + + QTemporaryDir m_dir; + + // Export the fixture to SVG and return the distinct positions of the + // junction dots drawn on it. + QSet dots() + { + const QString home = m_dir.filePath(QStringLiteral("home")); + const QString out = m_dir.filePath(QStringLiteral("svg")); + 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/junction_dot_shared_potential.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()); + + // A junction dot on a wire of the default width is a circle + // 3.0 across; the symbols in the fixture draw none that size. + static const QRegularExpression re( + QStringLiteral("")); + QSet found; + auto it = re.globalMatch(svg); + while (it.hasNext()) { + const auto m = it.next(); + found << m.captured(1) + QLatin1Char(',') + m.captured(2); + } + return found; + } + +private slots: + void initTestCase() + { + QVERIFY(m_dir.isValid()); + QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH))); + } + + void dotAtEveryT() + { + const QSet expected{QStringLiteral("200,260"), + QStringLiteral("300,260")}; + QCOMPARE(dots(), expected); + } +}; + +QTEST_GUILESS_MAIN(tst_junctiondots) +#include "tst_junctiondots.moc"