From d4644286429481b5ee3c0b1ddeac27e432b860be Mon Sep 17 00:00:00 2001 From: Beat Hangartner Date: Mon, 5 Oct 2026 18:00:26 +0200 Subject: [PATCH 1/3] Export wrapped texts to DXF line by line A symbol text with a width (text_width) is drawn wrapped on the folio, but the DXF export split texts on '\n' only, so it was written as one long line, running past its box and over the drawing. The export now writes the lines of folio texts as they are laid out (TextLines::layoutLines(), new header-only textlines.h). Texts without a width give the same lines as before. Tests: tst_textlines, tst_dxfwrappedtext (fails without this change). Co-Authored-By: Claude Opus 5.5 --- sources/dxfexport.cpp | 4 +- sources/textlines.h | 60 +++++++++++++++++++++ tests/qttest/CMakeLists.txt | 18 +++++++ tests/qttest/fixtures/dxf_wrapped_text.qet | 61 ++++++++++++++++++++++ tests/qttest/tst_dxfwrappedtext.cpp | 60 +++++++++++++++++++++ tests/qttest/tst_textlines.cpp | 54 +++++++++++++++++++ 6 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 sources/textlines.h create mode 100644 tests/qttest/fixtures/dxf_wrapped_text.qet create mode 100644 tests/qttest/tst_dxfwrappedtext.cpp create mode 100644 tests/qttest/tst_textlines.cpp diff --git a/sources/dxfexport.cpp b/sources/dxfexport.cpp index 5f3c266ff..6db86c5b4 100644 --- a/sources/dxfexport.cpp +++ b/sources/dxfexport.cpp @@ -35,6 +35,7 @@ #include "qetgraphicsitem/independenttextitem.h" #include "qetgraphicsitem/qetshapeitem.h" #include "qetgraphicsitem/terminal.h" +#include "textlines.h" #include #include @@ -358,7 +359,8 @@ void DxfExport::write(Diagram *diagram, int width, int height, qreal y = dti->scenePos().y() - ydir * fontSize * 1.8 - xdir * fontSize * 0.9; - QStringList lines = dti -> toPlainText().split('\n'); + //As drawn: a text with a width is wrapped + QStringList lines = TextLines::layoutLines(dti -> document()); qreal offset = fontSize * 1.6; foreach (QString line, lines) { if (line.size() > 0 && line != "_" ) diff --git a/sources/textlines.h b/sources/textlines.h new file mode 100644 index 000000000..e9a15e130 --- /dev/null +++ b/sources/textlines.h @@ -0,0 +1,60 @@ +/* + 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 . +*/ +#ifndef TEXTLINES_H +#define TEXTLINES_H + +#include +#include +#include +#include + +namespace TextLines +{ + /** + @return the lines of document as they are laid out: a paragraph + wrapped to the width of the text gives several lines. For the + exports that write a text line by line (DXF). + */ + inline QStringList layoutLines(const QTextDocument *document) + { + document->size(); //Lay the document out + + QStringList lines; + for (QTextBlock block = document->begin() ; block.isValid() ; block = block.next()) + { + const QString text = block.text(); + const QTextLayout *layout = block.layout(); + if (!layout || layout->lineCount() == 0) { + lines << text; + continue; + } + for (int i = 0 ; i < layout->lineCount() ; ++i) + { + const QTextLine line = layout->lineAt(i); + QString part = text.mid(line.textStart(), line.textLength()); + //The space the line was broken at, or a line separator + while (!part.isEmpty() && part.back().isSpace()) + part.chop(1); + lines << part; + } + } + return lines; + } +} + +#endif // TEXTLINES_H diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 3b7e8c2b5..f5182993d 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -104,6 +104,14 @@ add_test(NAME tst_textgrid COMMAND tst_textgrid) target_include_directories(tst_textgrid PRIVATE ${QET_DIR}/sources) target_link_libraries(tst_textgrid PRIVATE Qt::Test) +# textlines.h is header-only: the lines of a text as they are laid out, +# for the exports that write a text line by line. +add_executable(tst_textlines tst_textlines.cpp) +add_test(NAME tst_textlines COMMAND tst_textlines) +set_tests_properties(tst_textlines PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen") +target_include_directories(tst_textlines PRIVATE ${QET_DIR}/sources) +target_link_libraries(tst_textlines PRIVATE Qt::Test Qt::Gui) + # elementviewgrid.h is header-only: the symbol editor's snap step at each # zoom (bugtracker #112), tested without an editor window. add_executable(tst_elementviewgrid tst_elementviewgrid.cpp) @@ -403,6 +411,16 @@ target_compile_definitions(tst_wiringlistexport PRIVATE "QET_TEST_BINARY_PATH=\"$\"" "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") +# --export-dxf writes a wrapped symbol text line by line, as it is drawn. +add_executable( + tst_dxfwrappedtext + tst_dxfwrappedtext.cpp) +add_test(NAME tst_dxfwrappedtext COMMAND tst_dxfwrappedtext) +add_dependencies(tst_dxfwrappedtext qelectrotech) +target_link_libraries(tst_dxfwrappedtext PRIVATE Qt::Test) +target_compile_definitions(tst_dxfwrappedtext PRIVATE + "QET_TEST_BINARY_PATH=\"$\"") + # --export-pdf puts an A3 folio on an A3 page whichever way it is turned: # QPageSize matches standard sheets upright only, so a wide folio is matched # upright and turned. fixtures/pdf_page_a3.qet has one folio each way. diff --git a/tests/qttest/fixtures/dxf_wrapped_text.qet b/tests/qttest/fixtures/dxf_wrapped_text.qet new file mode 100644 index 000000000..920aebc74 --- /dev/null +++ b/tests/qttest/fixtures/dxf_wrapped_text.qet @@ -0,0 +1,61 @@ + + + 10/2/26 + 02-10-2026 + 2026-10-02 + skeleton + /tmp/qet-mcp-new-k41dmnyo/skeleton.qet + 18:39 + + + + + + + + + + + + + + + + + + + + + + + + + + WrapAlpha WrapBeta WrapGamma WrapDelta WrapEpsilon + LineAlpha LineBeta LineGamma LineDelta LineEpsilon + + + + + + + + + Imported elements + + + + + + Wrapped symbol text + + + + WrapAlpha WrapBeta WrapGamma WrapDelta WrapEpsilon + LineAlpha LineBeta LineGamma LineDelta LineEpsilon + + + + + + diff --git a/tests/qttest/tst_dxfwrappedtext.cpp b/tests/qttest/tst_dxfwrappedtext.cpp new file mode 100644 index 000000000..857c32084 --- /dev/null +++ b/tests/qttest/tst_dxfwrappedtext.cpp @@ -0,0 +1,60 @@ +/* + 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 + +/** + --export-dxf writes a symbol text wrapped to its width line by line, + as it is drawn, rather than as one long line. A text without a width + is still written on one line. +*/ +class tst_dxfwrappedtext : public QObject +{ + Q_OBJECT + +private slots: + void wrappedLines() + { + const QString project = QFINDTESTDATA("fixtures/dxf_wrapped_text.qet"); + QVERIFY2(!project.isEmpty(), "fixture project not found"); + QTemporaryDir dir; + QVERIFY(dir.isValid()); + + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); + QProcess proc; + proc.setProcessEnvironment(env); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--export-dxf"), project, dir.path()}); + QVERIFY2(proc.waitForFinished(60000), "--export-dxf timed out"); + QCOMPARE(proc.exitCode(), 0); + + QFile file(dir.filePath(QStringLiteral("01_diagram.dxf"))); + QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text)); + const QStringList values = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n')); + + //One text entity per word of the text 70 px wide... + for (const QString &word : {QStringLiteral("WrapAlpha"), QStringLiteral("WrapGamma"), QStringLiteral("WrapEpsilon")}) + QVERIFY2(values.contains(word), qPrintable(word)); + //...one for the whole text without a width + QVERIFY(values.contains(QStringLiteral("LineAlpha LineBeta LineGamma LineDelta LineEpsilon"))); + } +}; + +QTEST_APPLESS_MAIN(tst_dxfwrappedtext) +#include "tst_dxfwrappedtext.moc" diff --git a/tests/qttest/tst_textlines.cpp b/tests/qttest/tst_textlines.cpp new file mode 100644 index 000000000..fb987c694 --- /dev/null +++ b/tests/qttest/tst_textlines.cpp @@ -0,0 +1,54 @@ +/* + 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 "textlines.h" + +class tst_textlines : public QObject +{ + Q_OBJECT + +private slots: + // A text without a width gives its own lines. + void unwrappedTextGivesItsLines() + { + QTextDocument document(QStringLiteral("Motor protection\nswitch Q12")); + QCOMPARE(TextLines::layoutLines(&document), + QStringList({QStringLiteral("Motor protection"), QStringLiteral("switch Q12")})); + } + + // A text wrapped to its width gives one line per drawn line, without + // the space the line was broken at. + void wrappedTextGivesTheDrawnLines() + { + QTextDocument document(QStringLiteral("Motor protection switch Q12")); + QTextOption option = document.defaultTextOption(); + option.setWrapMode(QTextOption::WordWrap); + document.setDefaultTextOption(option); + document.setTextWidth(1); + + QCOMPARE(TextLines::layoutLines(&document), + QStringList({QStringLiteral("Motor"), QStringLiteral("protection"), + QStringLiteral("switch"), QStringLiteral("Q12")})); + } +}; + +QTEST_MAIN(tst_textlines) +#include "tst_textlines.moc" From 4b187e049fe5ecb08b4f6a29ae9d143c88ae8998 Mon Sep 17 00:00:00 2001 From: Beat Hangartner Date: Tue, 6 Oct 2026 09:53:49 +0200 Subject: [PATCH 2/3] Keep the DXF text lines of master for texts that do not wrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit layoutLines() trimmed the trailing spaces of every line and kept non-breaking spaces, which the DXF export used to write as plain spaces (toPlainText()). A DXF R10 file has no code page, so a reader assuming Windows-1252 could show them as 'Â '. Now a paragraph that does not wrap gives exactly its line of toPlainText().split('\n') again (trailing spaces and tabs kept, non-breaking spaces as plain spaces,
as a line break). In a paragraph that wraps, every line loses its trailing spaces, so spaces that wrap to a line of their own give an empty line: not written, but it keeps the spacing of the next lines. Test: tst_textlines compares with toPlainText() for trailing spaces, tabs, non-breaking spaces,
and paragraphs, and checks wrapped lines (fails before). Co-Authored-By: Claude Opus 5.5 --- sources/textlines.h | 33 ++++++++++++++---- tests/qttest/tst_textlines.cpp | 61 ++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/sources/textlines.h b/sources/textlines.h index e9a15e130..8d3382382 100644 --- a/sources/textlines.h +++ b/sources/textlines.h @@ -29,6 +29,11 @@ namespace TextLines @return the lines of document as they are laid out: a paragraph wrapped to the width of the text gives several lines. For the exports that write a text line by line (DXF). + A paragraph that is not wrapped gives exactly its line of + toPlainText().split('\n'), as these exports wrote before: trailing + spaces are kept, and non-breaking spaces become plain spaces. In a + wrapped paragraph, the lines lose their trailing spaces (the one a + line was broken at, and a line made only of spaces becomes empty). */ inline QStringList layoutLines(const QTextDocument *document) { @@ -39,18 +44,32 @@ namespace TextLines { const QString text = block.text(); const QTextLayout *layout = block.layout(); - if (!layout || layout->lineCount() == 0) { - lines << text; + const int count = layout ? layout->lineCount() : 0; + if (count == 0) { + lines << QString(text).replace(QChar::Nbsp, QLatin1Char(' ')); continue; } - for (int i = 0 ; i < layout->lineCount() ; ++i) + + //Wrapped: a line ends without a line break typed by the user + bool wrapped = false; + for (int i = 0 ; i < count - 1 ; ++i) { + const QTextLine line = layout->lineAt(i); + const int end = line.textStart() + line.textLength(); + if (end == 0 || text.at(end - 1) != QChar::LineSeparator) + wrapped = true; + } + + for (int i = 0 ; i < count ; ++i) { const QTextLine line = layout->lineAt(i); QString part = text.mid(line.textStart(), line.textLength()); - //The space the line was broken at, or a line separator - while (!part.isEmpty() && part.back().isSpace()) - part.chop(1); - lines << part; + if (part.endsWith(QChar::LineSeparator)) + part.chop(1); //A line break typed by the user (Shift+Enter) + if (wrapped) { + while (!part.isEmpty() && part.back().isSpace()) + part.chop(1); + } + lines << part.replace(QChar::Nbsp, QLatin1Char(' ')); } } return lines; diff --git a/tests/qttest/tst_textlines.cpp b/tests/qttest/tst_textlines.cpp index fb987c694..e9bd25ec3 100644 --- a/tests/qttest/tst_textlines.cpp +++ b/tests/qttest/tst_textlines.cpp @@ -34,6 +34,32 @@ private slots: QStringList({QStringLiteral("Motor protection"), QStringLiteral("switch Q12")})); } + // A text without a width gives exactly the lines the DXF export wrote + // before, toPlainText().split('\n'): trailing spaces and tabs kept, + // non-breaking spaces as plain spaces,
as a line break. + void unwrappedTextIsUnchanged_data() + { + QTest::addColumn("html"); + QTest::newRow("trailing spaces") << QStringLiteral("K1 \nswitch\t"); + QTest::newRow("non-breaking spaces") << QStringLiteral("

K1  24V DC

"); + QTest::newRow("line break") << QStringLiteral("

Wiper to 6
contacts

"); + QTest::newRow("paragraphs") << QStringLiteral("

relais voyant

preventa

"); + QTest::newRow("empty") << QString(); + } + + void unwrappedTextIsUnchanged() + { + QFETCH(QString, html); + QTextDocument document; + if (Qt::mightBeRichText(html)) + document.setHtml(html); + else + document.setPlainText(html); + + QCOMPARE(TextLines::layoutLines(&document), + document.toPlainText().split(QLatin1Char('\n'))); + } + // A text wrapped to its width gives one line per drawn line, without // the space the line was broken at. void wrappedTextGivesTheDrawnLines() @@ -48,6 +74,41 @@ private slots: QStringList({QStringLiteral("Motor"), QStringLiteral("protection"), QStringLiteral("switch"), QStringLiteral("Q12")})); } + + // A wrapped paragraph has no trailing spaces left, even on its last + // line: tabs and spaces that wrap to a line of their own (as in the + // affuteuse_250h example) give an empty line, which is not written but + // still keeps the line spacing. + void wrappedTextHasNoTrailingSpaces() + { + QTextDocument document(QStringLiteral("\tMotor\t\t ")); + QTextOption option = document.defaultTextOption(); + option.setWrapMode(QTextOption::WordWrap); + document.setDefaultTextOption(option); + document.setTextWidth(80); + + const QStringList lines = TextLines::layoutLines(&document); + QVERIFY(lines.size() > 1); + QVERIFY(lines.contains(QStringLiteral("Motor"))); + for (const QString &line : lines) + QVERIFY2(line.isEmpty() || !line.back().isSpace(), qPrintable(line)); + } + + // Non-breaking spaces also become plain spaces in a wrapped text. + void wrappedTextHasPlainSpaces() + { + QTextDocument document; + document.setHtml(QStringLiteral("

K1 24V and more words

")); + QTextOption option = document.defaultTextOption(); + option.setWrapMode(QTextOption::WordWrap); + document.setDefaultTextOption(option); + document.setTextWidth(1); + + const QStringList lines = TextLines::layoutLines(&document); + QCOMPARE(lines.first(), QStringLiteral("K1 24V")); + for (const QString &line : lines) + QVERIFY(!line.contains(QChar::Nbsp)); + } }; QTEST_MAIN(tst_textlines) From f6ac685ebec176627713d61635c11d8c75cf7cda Mon Sep 17 00:00:00 2001 From: Beat Hangartner Date: Tue, 6 Oct 2026 09:53:49 +0200 Subject: [PATCH 3/3] Drop a machine-specific path from the DXF test fixture Co-Authored-By: Claude Opus 5.5 --- tests/qttest/fixtures/dxf_wrapped_text.qet | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/qttest/fixtures/dxf_wrapped_text.qet b/tests/qttest/fixtures/dxf_wrapped_text.qet index 920aebc74..25fdaf656 100644 --- a/tests/qttest/fixtures/dxf_wrapped_text.qet +++ b/tests/qttest/fixtures/dxf_wrapped_text.qet @@ -3,8 +3,7 @@ 10/2/26 02-10-2026 2026-10-02 - skeleton - /tmp/qet-mcp-new-k41dmnyo/skeleton.qet + dxf_wrapped_text 18:39