From 9136c605f4b1d293f2db7eb75d0de75f24f61ee0 Mon Sep 17 00:00:00 2001 From: Beat Hangartner Date: Fri, 9 Oct 2026 13:35:53 +0200 Subject: [PATCH] Load a free text's non-finite position or rotation as 0 "nan", "inf" and "-inf" parse as numbers, and IndependentTextItem:: fromXml() used them as they came for x, y and rotation. A project with such a value kept it: --resave wrote x="nan" back, and the SVG export drew the text at matrix(1,0,0,1,nan,95), which is not valid SVG. Conductors and element texts already load a non-finite position as 0. Load a non-finite x, y or rotation of a free text as 0, the same way. No file-format change: finite values load and save as before. Tests: tst_freetextnonfinite runs the binary's --resave and --export-svg on a fixture with x="nan", y="inf", x="-inf", rotation="nan" and a finite text (61.7, 61.3, 37 degrees, kept as they are). The four non-finite cases and the export check fail on master. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Beat Hangartner --- .../qetgraphicsitem/independenttextitem.cpp | 9 +- tests/qttest/CMakeLists.txt | 13 ++ tests/qttest/fixtures/free_text_nonfinite.qet | 43 ++++++ tests/qttest/tst_freetextnonfinite.cpp | 129 ++++++++++++++++++ 4 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 tests/qttest/fixtures/free_text_nonfinite.qet create mode 100644 tests/qttest/tst_freetextnonfinite.cpp diff --git a/sources/qetgraphicsitem/independenttextitem.cpp b/sources/qetgraphicsitem/independenttextitem.cpp index 833b8dd48..7dfef0047 100644 --- a/sources/qetgraphicsitem/independenttextitem.cpp +++ b/sources/qetgraphicsitem/independenttextitem.cpp @@ -85,9 +85,14 @@ IndependentTextItem::~IndependentTextItem() void IndependentTextItem::fromXml(const QDomElement &e) { const QUuid uuid(e.attribute(QStringLiteral("uuid"))); if (!uuid.isNull() && uuid != m_uuid) setUuid(uuid); - setPos(e.attribute("x").toDouble(), e.attribute("y").toDouble()); + //"nan" and "inf" parse as numbers; a non-finite position or + //rotation is kept at 0, as for conductors and element texts + const qreal x = e.attribute("x").toDouble(); + const qreal y = e.attribute("y").toDouble(); + setPos(qIsFinite(x) ? x : 0, qIsFinite(y) ? y : 0); setHtml(e.attribute("text")); - setRotation(e.attribute("rotation").toDouble()); + const qreal rotation = e.attribute("rotation").toDouble(); + setRotation(qIsFinite(rotation) ? rotation : 0); if (e.hasAttribute("font")) { QFont font; diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 8653c86a4..fe45c2b4d 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -584,6 +584,19 @@ target_link_libraries(tst_freetextwidth PRIVATE Qt::Test Qt::Gui Qt::Xml) target_compile_definitions(tst_freetextwidth PRIVATE "QET_TEST_BINARY_PATH=\"$\"") +# A free text whose x, y or rotation is "nan" or "inf" is loaded at 0 for +# that number, so a resave and the SVG export carry no non-finite number: +# the real binary on fixtures/free_text_nonfinite.qet. +add_executable( + tst_freetextnonfinite + tst_freetextnonfinite.cpp) +add_test(NAME tst_freetextnonfinite COMMAND tst_freetextnonfinite) +set_tests_properties(tst_freetextnonfinite PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen") +add_dependencies(tst_freetextnonfinite qelectrotech) +target_link_libraries(tst_freetextnonfinite PRIVATE Qt::Test Qt::Xml) +target_compile_definitions(tst_freetextnonfinite 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 diff --git a/tests/qttest/fixtures/free_text_nonfinite.qet b/tests/qttest/fixtures/free_text_nonfinite.qet new file mode 100644 index 000000000..801f0a3a8 --- /dev/null +++ b/tests/qttest/fixtures/free_text_nonfinite.qet @@ -0,0 +1,43 @@ + + + 10/2/26 + 02-10-2026 + 2026-10-02 + free_text_nonfinite + 18:39 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Imported elements + + + + diff --git a/tests/qttest/tst_freetextnonfinite.cpp b/tests/qttest/tst_freetextnonfinite.cpp new file mode 100644 index 000000000..3e1881f26 --- /dev/null +++ b/tests/qttest/tst_freetextnonfinite.cpp @@ -0,0 +1,129 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include +#include +#include +#include +#include +#include + +/** + A free text () whose x, y or rotation is "nan", "inf" or "-inf" + is loaded at 0 for that number, as conductors and element texts are. + Those strings parse as numbers, so the text used to keep them: a resave + wrote them back, and the SVG export drew the text at translate(nan,...). + Runs the real binary's --resave and --export-svg on + fixtures/free_text_nonfinite.qet. +*/ +class tst_freetextnonfinite : public QObject +{ + Q_OBJECT + + QTemporaryDir m_dir; + QString m_fixture; + + static bool runQet(const QStringList &arguments) + { + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); + QProcess proc; + proc.setProcessEnvironment(env); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), arguments); + return proc.waitForFinished(60000) + && proc.exitStatus() == QProcess::NormalExit + && proc.exitCode() == 0; + } + + /// The saved whose text contains @p word. + static QDomElement input(const QDomDocument &doc, const QString &word) + { + const QDomNodeList list = doc.elementsByTagName(QStringLiteral("input")); + for (int i = 0; i < list.size(); ++i) { + const QDomElement e = list.at(i).toElement(); + if (e.attribute(QStringLiteral("text")).contains(word)) return e; + } + return QDomElement(); + } + +private slots: + void initTestCase() + { + QVERIFY2(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)), "qelectrotech binary not found"); + QVERIFY(m_dir.isValid()); + m_fixture = QFINDTESTDATA("fixtures/free_text_nonfinite.qet"); + QVERIFY2(!m_fixture.isEmpty(), "fixture project not found"); + } + + void savedNumbersAreFinite_data() + { + QTest::addColumn("word"); + QTest::addColumn("x"); + QTest::addColumn("y"); + QTest::addColumn("rotation"); + QTest::newRow("x nan") << "NanX" << "0" << "100" << "0"; + QTest::newRow("y inf") << "InfY" << "200" << "0" << "0"; + QTest::newRow("x -inf") << "MinusInfX" << "0" << "300" << "0"; + QTest::newRow("rotation nan") << "NanRotation" << "400" << "400" << "0"; + // A finite text, with values that round differently, is kept as it is. + QTest::newRow("finite") << "Plain" << "61.7" << "61.3" << "37"; + } + + void savedNumbersAreFinite() + { + QFETCH(QString, word); + QFETCH(QString, x); + QFETCH(QString, y); + QFETCH(QString, rotation); + + const QString out = m_dir.filePath(QStringLiteral("resaved-%1.qet").arg(word)); + QVERIFY2(runQet({QStringLiteral("--resave"), m_fixture, out}), "--resave failed"); + QFile file(out); + QVERIFY(file.open(QIODevice::ReadOnly)); + QDomDocument doc; + QVERIFY(doc.setContent(&file)); + + const QDomElement e = input(doc, word); + QVERIFY2(!e.isNull(), "text not found in the resaved project"); + QCOMPARE(e.attribute(QStringLiteral("x")), x); + QCOMPARE(e.attribute(QStringLiteral("y")), y); + QCOMPARE(e.attribute(QStringLiteral("rotation")), rotation); + } + + // No "nan" or "inf" reaches the exported drawing. + void exportHasNoNonFiniteNumbers() + { + const QString dir = m_dir.filePath(QStringLiteral("svg")); + QVERIFY(QDir().mkpath(dir)); + QVERIFY2(runQet({QStringLiteral("--export-svg"), m_fixture, dir}), "--export-svg failed"); + const QStringList files = QDir(dir).entryList({QStringLiteral("*.svg")}, QDir::Files); + QCOMPARE(files.size(), 1); + QFile svg(QDir(dir).filePath(files.first())); + QVERIFY(svg.open(QIODevice::ReadOnly)); + const QString content = QString::fromUtf8(svg.readAll()); + const QRegularExpression non_finite(QStringLiteral("[(,\\s=\"]-?(nan|inf)\\b"), + QRegularExpression::CaseInsensitiveOption); + const QRegularExpressionMatch match = non_finite.match(content); + QVERIFY2(!match.hasMatch(), + qPrintable(QStringLiteral("found \"%1\" in the SVG").arg( + content.mid(qMax(0, int(match.capturedStart()) - 30), 60)))); + QVERIFY(content.contains(QStringLiteral("NanX"))); + } +}; + +QTEST_MAIN(tst_freetextnonfinite) +#include "tst_freetextnonfinite.moc"