From 166ee34f5279f1da395f53d0dbda9fa00f510549 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 27 Sep 2026 21:46:30 +1300 Subject: [PATCH 1/6] Fix parts lists saved before June 2022 losing rows and tables on open A nomenclature table saved by an older version filters on the element type names the project database used then: element_type = 'Simple', 'Terminale', 'Master'. Commit 2e70d2e59 (June 2022) changed the database to "simple", "terminal", "master", and SQLite compares text case-sensitively, so such a table silently lost every row of that type on open. Its continuation tables were then empty, and removeUselessNextTable() deleted them: opening and saving the example industrial.qet removed seven of its ten parts-list tables (folios 44-50), and the remaining three listed 76 of 258 parts. ProjectDBModel::fromXml() now rewrites old names in element_type = '...' comparisons to the current ones (LegacyElementTypes::upgradeQuery()), which also lets the query editor tick the right boxes again. A query saved by a current version is unchanged, and so is any other text that happens to contain "Simple". Checked in the GUI on industrial.qet, open then save: master keeps tables on 3 of folios 41-50, this keeps all 10, with 258 rows (the last table 24 of 26) and the query saved as 'simple'. tst_legacyelementtypes fails when a name maps wrongly. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG --- cmake/qet_compilation_vars.cmake | 1 + sources/dataBase/legacyelementtypes.h | 69 +++++++++++++++++++ .../ViewItem/projectdbmodel.cpp | 5 +- tests/qttest/CMakeLists.txt | 7 ++ tests/qttest/tst_legacyelementtypes.cpp | 60 ++++++++++++++++ 5 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 sources/dataBase/legacyelementtypes.h create mode 100644 tests/qttest/tst_legacyelementtypes.cpp diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 60fddcf55..398cdce8c 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -325,6 +325,7 @@ set(QET_SRC_FILES ${QET_DIR}/sources/autoNum/ui/selectautonumw.cpp ${QET_DIR}/sources/autoNum/ui/selectautonumw.h + ${QET_DIR}/sources/dataBase/legacyelementtypes.h ${QET_DIR}/sources/dataBase/projectdatabase.cpp ${QET_DIR}/sources/dataBase/projectdatabase.h ${QET_DIR}/sources/dataBase/sqlreadonly.cpp diff --git a/sources/dataBase/legacyelementtypes.h b/sources/dataBase/legacyelementtypes.h new file mode 100644 index 000000000..51943996b --- /dev/null +++ b/sources/dataBase/legacyelementtypes.h @@ -0,0 +1,69 @@ +/* + 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 LEGACYELEMENTTYPES_H +#define LEGACYELEMENTTYPES_H + +#include +#include + +/** + Element type names in queries saved by versions before June 2022. + The project database then held Element::linkTypeToString() ("Simple", + "Terminale", ...); since it holds ElementData::typeToString() + ("simple", "terminal", ...). SQLite compares text case-sensitively, so + a nomenclature table saved with element_type = 'Simple' silently lost + every simple element, and its continuation tables were deleted as + empty. +*/ +namespace LegacyElementTypes +{ + /** + @return query with every element_type = '' comparison + rewritten to the current name. Anything else is left as it is, + so a query saved by a current version comes back unchanged. + */ + inline QString upgradeQuery(const QString &query) + { + static const QRegularExpression re(QStringLiteral( + "element_type\\s*=\\s*'(Simple|NextReport|PreviousReport|Master" + "|Slave|Terminale|Thumbnail)'")); + + QString upgraded = query; + auto it = re.globalMatch(query); + int shift = 0; + while (it.hasNext()) + { + const auto match = it.next(); + const QString old_name = match.captured(1); + QString new_name; + if (old_name == QLatin1String("Simple")) new_name = QStringLiteral("simple"); + else if (old_name == QLatin1String("NextReport")) new_name = QStringLiteral("next_report"); + else if (old_name == QLatin1String("PreviousReport")) new_name = QStringLiteral("previous_report"); + else if (old_name == QLatin1String("Master")) new_name = QStringLiteral("master"); + else if (old_name == QLatin1String("Slave")) new_name = QStringLiteral("slave"); + else if (old_name == QLatin1String("Terminale")) new_name = QStringLiteral("terminal"); + else new_name = QStringLiteral("thumbnail"); + + upgraded.replace(match.capturedStart(1) + shift, old_name.size(), new_name); + shift += new_name.size() - old_name.size(); + } + return upgraded; + } +} + +#endif // LEGACYELEMENTTYPES_H diff --git a/sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp b/sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp index ca07e5ba8..a201fcb02 100644 --- a/sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp +++ b/sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp @@ -19,6 +19,7 @@ #include +#include "../../dataBase/legacyelementtypes.h" #include "../../dataBase/projectdatabase.h" #include "../../qetapp.h" #include "../../qetinformation.h" @@ -294,7 +295,9 @@ void ProjectDBModel::fromXml(const QDomElement &element) return; setIdentifier(element.firstChildElement("identifier").text()); - setQuery(element.firstChildElement("query").text()); + //A query saved before June 2022 names element types the old way + setQuery(LegacyElementTypes::upgradeQuery( + element.firstChildElement("query").text())); //Index 0,0 auto index_00 = element.firstChildElement("index00"); diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 66074282d..d2437661d 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -104,6 +104,13 @@ 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) +# legacyelementtypes.h is header-only: the rewrite of element type names in +# nomenclature queries saved before June 2022. +add_executable(tst_legacyelementtypes tst_legacyelementtypes.cpp) +add_test(NAME tst_legacyelementtypes COMMAND tst_legacyelementtypes) +target_include_directories(tst_legacyelementtypes PRIVATE ${QET_DIR}/sources) +target_link_libraries(tst_legacyelementtypes PRIVATE Qt::Test) + add_executable( tst_qetpalette tst_qetpalette.cpp diff --git a/tests/qttest/tst_legacyelementtypes.cpp b/tests/qttest/tst_legacyelementtypes.cpp new file mode 100644 index 000000000..bc36725e1 --- /dev/null +++ b/tests/qttest/tst_legacyelementtypes.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 "dataBase/legacyelementtypes.h" + +#include + +class TestLegacyElementTypes : public QObject +{ + Q_OBJECT + + private slots: + void upgradesOldNames_data() + { + QTest::addColumn("saved"); + QTest::addColumn("expected"); + + // The WHERE clause industrial.qet's parts list was saved with. + QTest::newRow("simple, with sub types") + << "WHERE ( element_type = 'Simple' OR element_sub_type = 'coil') ORDER BY label" + << "WHERE ( element_type = 'simple' OR element_sub_type = 'coil') ORDER BY label"; + QTest::newRow("terminal and master, lengths change") + << "element_type = 'Terminale' OR element_type = 'Master' OR element_type = 'Simple'" + << "element_type = 'terminal' OR element_type = 'master' OR element_type = 'simple'"; + QTest::newRow("reports and thumbnail") + << "element_type='NextReport' OR element_type = 'PreviousReport' OR element_type = 'Thumbnail'" + << "element_type='next_report' OR element_type = 'previous_report' OR element_type = 'thumbnail'"; + QTest::newRow("current names unchanged") + << "element_type = 'simple' OR element_type = 'terminal'" + << "element_type = 'simple' OR element_type = 'terminal'"; + QTest::newRow("the word elsewhere unchanged") + << "WHERE label = 'Simple' OR designation = 'Master'" + << "WHERE label = 'Simple' OR designation = 'Master'"; + QTest::newRow("empty") << "" << ""; + } + + void upgradesOldNames() + { + QFETCH(QString, saved); + QFETCH(QString, expected); + QCOMPARE(LegacyElementTypes::upgradeQuery(saved), expected); + } +}; + +QTEST_APPLESS_MAIN(TestLegacyElementTypes) +#include "tst_legacyelementtypes.moc" From 4278ef864eb18a9cdf59b6a487df757b5ae7eae1 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 27 Sep 2026 19:03:17 +1300 Subject: [PATCH 2/6] Add DXF export from the command line (--export-dxf) qelectrotech --export-dxf [--show-terminals] writes one DXF per folio, named _.dxf like the PNG and SVG exports. Discussion #1072. The DXF code moves out of ExportDialog into DxfExport, unchanged except that its options come from an ExportProperties argument instead of the dialog. The dialog and the command line both call it, and the command line uses the dialog's default options (the preferences' export settings), so both write the same entities. - Createdxf answers a file it cannot open with a message box and exit(0); the command line checks the file first and fails with a message and exit code 1 instead. - A note is printed when pictures become outline boxes, as the dialog warns. - Scripting: qet.exportDxf(outDir, showTerminals). MCP: format "dxf". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG --- cmake/qet_compilation_vars.cmake | 2 + misc/qet-mcp/README.md | 2 +- misc/qet-mcp/qet_mcp.py | 3 +- sources/cli_export.cpp | 57 ++++ sources/cli_export.h | 8 +- sources/createdxf.cpp | 4 +- sources/dxfexport.cpp | 438 +++++++++++++++++++++++++++++ sources/dxfexport.h | 42 +++ sources/exportdialog.cpp | 383 +------------------------ sources/exportdialog.h | 2 - sources/scripting/qetscriptapi.cpp | 7 + sources/scripting/qetscriptapi.h | 1 + 12 files changed, 562 insertions(+), 387 deletions(-) create mode 100644 sources/dxfexport.cpp create mode 100644 sources/dxfexport.h diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 60fddcf55..2468d57bb 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -192,6 +192,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/configdialog.h ${QET_DIR}/sources/createdxf.cpp ${QET_DIR}/sources/createdxf.h + ${QET_DIR}/sources/dxfexport.cpp + ${QET_DIR}/sources/dxfexport.h ${QET_DIR}/sources/dxfpaintdevice.cpp ${QET_DIR}/sources/dxfpaintdevice.h ${QET_DIR}/sources/diagramcommands.cpp diff --git a/misc/qet-mcp/README.md b/misc/qet-mcp/README.md index 6ab17ffe9..a32c453a2 100644 --- a/misc/qet-mcp/README.md +++ b/misc/qet-mcp/README.md @@ -33,7 +33,7 @@ here read the model. | `qet_diff` | **what an edit actually changed** — element moves, adds, removes, relabels; conductor changes; and folio fields, texts, shapes, images, symbol text fields and terminal strips | | `qet_scan` | sweep a directory of projects, counting nodes carrying an attribute | | `qet_element_info` | a `.elmt`: translated names, terminals, info fields, part counts | -| `qet_export` | run a headless export (pdf, png, svg, bom, cables, wires, wiring, nets, links, info) | +| `qet_export` | run a headless export (pdf, png, svg, dxf, bom, cables, wires, wiring, nets, links, info) | | `qet_edit` | **change a project** — place, move, rotate, label, wire, number, cross-reference, add text, shapes and images, restyle a symbol's text fields, delete; then diff the result | | `qet_element_build` | **author a `.elmt`** — draw a new symbol, with terminals to wire it by | | `qet_project_new` | **start from nothing** — an empty project with a title and folios | diff --git a/misc/qet-mcp/qet_mcp.py b/misc/qet-mcp/qet_mcp.py index 73035ceba..fef21791c 100755 --- a/misc/qet-mcp/qet_mcp.py +++ b/misc/qet-mcp/qet_mcp.py @@ -69,6 +69,7 @@ EXPORT_FORMATS = { "pdf": "--export-pdf", "png": "--export-png", "svg": "--export-svg", + "dxf": "--export-dxf", "bom": "--export-bom", "cables": "--export-cables", "wires": "--export-wires", @@ -2186,7 +2187,7 @@ TOOLS = [ }, { "name": "qet_export", - "description": "Run a QElectroTech export headlessly (pdf, png, svg, bom, " + "description": "Run a QElectroTech export headlessly (pdf, png, svg, dxf, bom, " "cables, wires, wiring, nets, links, info). Launches the " "binary in an isolated sandbox so it cannot be captured by, " "or capture, a running QElectroTech.", diff --git a/sources/cli_export.cpp b/sources/cli_export.cpp index aa6c96052..9f5b0e16b 100644 --- a/sources/cli_export.cpp +++ b/sources/cli_export.cpp @@ -24,8 +24,11 @@ #include "dataBase/projectdatabase.h" #include "diagram.h" #include "diagramcontext.h" +#include "dxfexport.h" +#include "exportproperties.h" #include "pdf_links.h" #include "qetgraphicsitem/conductor.h" +#include "qetgraphicsitem/diagramimageitem.h" #include "qetgraphicsitem/element.h" #include "qetgraphicsitem/terminal.h" #include "qetproject.h" @@ -69,6 +72,7 @@ const QHash<QString, QString> &exportFlags() {"--export-pdf", "pdf"}, {"--export-png", "png"}, {"--export-svg", "svg"}, + {"--export-dxf", "dxf"}, {"--export-cables", "cables"}, {"--export-wires", "wires"}, {"--export-bom", "bom"}, @@ -279,6 +283,57 @@ int exportImages(QETProject &project, const QString &format, return 0; } +/// One DXF file per diagram, written by the same code as the export dialog +/// (DxfExport) with the dialog's default options -- the export settings of +/// the preferences -- so both give the same file. +int exportDxf(QETProject &project, const QString &out_dir, bool showTerminals) +{ + const QList<Diagram *> diagrams = project.diagrams(); + if (diagrams.isEmpty()) { + err << "No diagrams to export.\n"; + return 1; + } + QDir().mkpath(out_dir); + + ExportProperties properties = ExportProperties::defaultExportProperties(); + properties.format = QStringLiteral("DXF"); + if (showTerminals) + properties.draw_terminals = true; + + int index = 0; + bool has_images = false; + for (Diagram *diagram : diagrams) { + ++index; + const QString path = QDir(out_dir).filePath( + diagramStem(diagram, index) + ".dxf"); + + // Createdxf answers a file it cannot open with a message box and + // exit(0), which headless means no message and a success code. + QFile probe(path); + if (!probe.open(QIODevice::WriteOnly)) { + err << "Cannot open '" << path << "' for writing.\n"; + return 1; + } + probe.close(); + + const QSize size = DxfExport::folioSize(diagram, properties); + DxfExport::write(diagram, size.width(), size.height(), path, properties); + + for (QGraphicsItem *item : diagram->items()) { + if (qgraphicsitem_cast<DiagramImageItem *>(item)) { + has_images = true; + break; + } + } + out << " " << path << "\n"; + } + if (has_images) + err << "Note: DXF has no pictures in this format; " + "each picture is exported as an outline box.\n"; + out << "Exported " << diagrams.size() << " diagram(s) -> " << out_dir << "\n"; + return 0; +} + int exportCsv(QETProject &project, const QString &format, const QString &output) { QString csv; @@ -890,6 +945,8 @@ int run(const QStringList &args) } if (format == "pdf") return exportPdf(project, output, showTerminals); + if (format == "dxf") + return exportDxf(project, output, showTerminals); if (format == "cables" || format == "wires") return exportCsv(project, format, output); if (format == "bom") diff --git a/sources/cli_export.h b/sources/cli_export.h index 2c2bc7d81..8ad1b1d12 100644 --- a/sources/cli_export.h +++ b/sources/cli_export.h @@ -45,6 +45,7 @@ namespace CLIExport { qelectrotech --export-pdf <project.qet> <output.pdf> [--show-terminals] qelectrotech --export-png <project.qet> <output_dir> [--show-terminals] qelectrotech --export-svg <project.qet> <output_dir> [--show-terminals] + qelectrotech --export-dxf <project.qet> <output_dir> [--show-terminals] qelectrotech --export-cables <project.qet> <output.csv> qelectrotech --export-wires <project.qet> <output.csv> qelectrotech --export-bom <project.qet> <output.csv> @@ -57,11 +58,14 @@ namespace CLIExport { qelectrotech --set-titleblock <project.qet> <output.qet> key=value... PDF: one multi-page document (one diagram per page). - PNG/SVG: one file per diagram, named <output_dir>/<NN>_<title>.<ext>. + PNG/SVG/DXF: one file per diagram, named <output_dir>/<NN>_<title>.<ext>. + DXF: the same file the export dialog writes with its default options + (the export settings of the preferences). --show-terminals: also paint terminal markers (red stroke + blue docking dot) and terminal names, as the interactive editor does; off by default, matching the GUI export dialog's - default. Has no effect on the non-image export modes. + default. For DXF, draws the terminal markers. Has no effect on + the list export modes. cables: wiring list (one row per conductor) as CSV. wires: list of distinct wire numbers as CSV. bom: bill of materials (one row per element) as CSV. diff --git a/sources/createdxf.cpp b/sources/createdxf.cpp index 1a82fb14f..28fff363e 100644 --- a/sources/createdxf.cpp +++ b/sources/createdxf.cpp @@ -20,7 +20,7 @@ #include <QTextStream> #include <QMessageBox> #include <QString> -#include "exportdialog.h" +#include "dxfexport.h" const double Createdxf::sheetWidth = 4000; @@ -583,7 +583,7 @@ void Createdxf::drawArcEllipse( arc_endAngle = temp; } - QPointF transformed_point = ExportDialog::rotation_transformed( + QPointF transformed_point = DxfExport::rotation_transformed( center_x, center_y, hotspot_x, diff --git a/sources/dxfexport.cpp b/sources/dxfexport.cpp new file mode 100644 index 000000000..7d3d8818e --- /dev/null +++ b/sources/dxfexport.cpp @@ -0,0 +1,438 @@ +/* + 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 <http://www.gnu.org/licenses/>. +*/ +#include "dxfexport.h" + +#include "conductorsegment.h" +#include "createdxf.h" +#include "diagram.h" +#include "dxfpaintdevice.h" +#include "exportproperties.h" +#include "factory/elementpicturefactory.h" +#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h" +#include "qetgraphicsitem/conductor.h" +#include "qetgraphicsitem/conductortextitem.h" +#include "qetgraphicsitem/crossrefitem.h" +#include "qetgraphicsitem/diagramimageitem.h" +#include "qetgraphicsitem/diagramtextitem.h" +#include "qetgraphicsitem/dynamicelementtextitem.h" +#include "qetgraphicsitem/element.h" +#include "qetgraphicsitem/independenttextitem.h" +#include "qetgraphicsitem/qetshapeitem.h" +#include "qetgraphicsitem/terminal.h" + +#include <QGraphicsSimpleTextItem> +#include <QSet> +#include <cmath> +#include <utility> + +/** + @brief DxfExport::folioSize + @return the size of @a diagram as exported with @a properties: its border + and title block, or just its items, following properties.exported_area. + The export dialog offers this size by default, and --export-dxf uses it. +*/ +QSize DxfExport::folioSize(Diagram *diagram, const ExportProperties &properties) +{ + const bool state_useBorder = diagram -> useBorder(); + diagram -> setUseBorder(properties.exported_area == QET::BorderArea); + const QSize size = diagram -> imageSize(); + diagram -> setUseBorder(state_useBorder); + return size; +} + +/** + @brief DxfExport::write + Export @a diagram as a DXF file. Moved here unchanged from + ExportDialog::generateDxf(), except that the options come from + @a properties instead of the dialog, so the command line can use it. + The diagram's own display options are applied for the export and put + back afterwards, as the dialog always did. + @param diagram : folio to export + @param width : width of the export, as offered by folioSize() + @param height : height of the export + @param path : file to write + @param properties : export options +*/ +void DxfExport::write(Diagram *diagram, int width, int height, + const QString &path, const ExportProperties &properties) +{ + //Non-const: BorderTitleBlock::drawDxf() takes a QString &. + QString file_path = path; + const ExportProperties previous = diagram -> applyProperties(properties); + + width -= 2*Diagram::margin; + height -= 2*Diagram::margin; + + Createdxf::xScale = Createdxf::sheetWidth / double(width); + Createdxf::yScale = Createdxf::sheetHeight / double(height); + + Createdxf::dxfBegin(file_path); + + //Add project elements (lines, rectangles, circles, texts) to dxf file + if (properties.draw_border) { + QRectF rect(Diagram::margin,Diagram::margin,width,height); + Createdxf::drawRectangle(file_path,rect,0); + } + diagram -> border_and_titleblock.drawDxf(file_path, 0); + + // Build the lists of elements. + QList<Element *> list_elements; + QList<Conductor *> list_conductors; + QList<DiagramTextItem *> list_texts; + QList<DiagramImageItem *> list_images; + //Slave cross-reference labels. They hang off a DynamicElementTextItem + //as plain QGraphicsTextItem children, so neither cast below picks them + //up and they were missing from the DXF entirely. + QList<QGraphicsTextItem *> list_xref_texts; + //Master-side cross-reference item (the table/cross drawn next to a + //report/master element). It paints itself with hand-written + //QPainter code across three modes (drawAsCross/drawAsContacts/ + //drawAsPlcTable), so instead of hand-porting each one it's replayed + //through DxfPaintEngine, which reuses paint() unmodified. + QList<CrossRefItem *> list_master_xrefs; + QList<QLineF *> list_lines; + QList<QRectF *> list_rectangles; + //QList<QRectF *> list_ellipses; + QList <QetShapeItem *> list_shapes; + QList <QetGraphicsTableItem *> list_tables; +// QList <Terminal *> list_terminals; + + // Determine les elements a "XMLiser" + //In stacking order (z, then insertion order, which a load makes the + //file's order), not items() order: that follows memory addresses, so + //the same folio came out in a different order on every run (FINDINGS + //F050), as saving did before bugtracker #343. A rect query gives + //stacking order even with NoIndex; anything it misses keeps its + //items() position after the rest. + QList<QGraphicsItem *> stacked_items = diagram -> items( + QRectF(-1e9, -1e9, 2e9, 2e9), Qt::IntersectsItemBoundingRect, + Qt::AscendingOrder); + { + const QSet<QGraphicsItem *> ranked(stacked_items.cbegin(), stacked_items.cend()); + for (QGraphicsItem *qgi : diagram -> items()) { + if (!ranked.contains(qgi)) { + stacked_items << qgi; + } + } + } + for (QGraphicsItem *qgi : std::as_const(stacked_items)) { + if (Element *elmt = qgraphicsitem_cast<Element *>(qgi)) { + list_elements << elmt; + } else if (Conductor *f = qgraphicsitem_cast<Conductor *>(qgi)) { + list_conductors << f; + } else if (IndependentTextItem *iti = qgraphicsitem_cast<IndependentTextItem *>(qgi)) { + list_texts << iti; + } else if (DiagramImageItem *dii = qgraphicsitem_cast<DiagramImageItem *>(qgi)) { + list_images << dii; + } else if (QetShapeItem *dii = qgraphicsitem_cast<QetShapeItem *>(qgi)) { + list_shapes << dii; + } else if (DynamicElementTextItem *deti = qgraphicsitem_cast<DynamicElementTextItem *>(qgi)) { + list_texts << deti; + if (QGraphicsTextItem *xref = deti->slaveXrefItem()) { + list_xref_texts << xref; + } + } else if (QetGraphicsTableItem *gti = qgraphicsitem_cast<QetGraphicsTableItem *>(qgi)) { + list_tables << gti; + } else if (CrossRefItem *xref = qgraphicsitem_cast<CrossRefItem *>(qgi)) { + list_master_xrefs << xref; + } + } + + // Draw shapes + foreach (QetShapeItem *qsi, list_shapes) qsi->toDXF(file_path, qsi->pen()); + + // Draw tables + foreach (QetGraphicsTableItem *gti, list_tables) { + gti->toDXF(file_path); + } + + //Draw elements + foreach(Element *elmt, list_elements) + { + double rotation_angle = elmt -> orientation() * 90; + + qreal elem_pos_x = elmt -> pos().x(); + qreal elem_pos_y = elmt -> pos().y();// - (diagram -> margin / 2); + + ElementPictureFactory::primitives primitives = ElementPictureFactory::instance()->getPrimitives(elmt->location()); + + for(QGraphicsSimpleTextItem *text : primitives.m_texts) + { + qreal fontSize = text->font().pointSizeF(); + if (fontSize < 0) + fontSize = text->font().pixelSize(); + + qreal x = elem_pos_x + text->pos().x(); + qreal y = elem_pos_y + text->pos().y(); + + qreal angle = text -> rotation() + rotation_angle; + qreal angler = angle * M_PI/180; + int xdir = -sin(angler); + int ydir = -cos(angler); + + QPointF transformed_point = rotation_transformed(x, y, elem_pos_x, elem_pos_y, -rotation_angle); + x = transformed_point.x() - ydir * fontSize * 0.5; + y = transformed_point.y() - xdir * fontSize * 0.5; + QStringList lines = text->text().split('\n'); + qreal offset = fontSize * 1.6; + for (QString line : lines) + { + if (line.size() > 0 && line != "_" ) { + Createdxf::drawText(file_path, line, QPointF(x, y), fontSize, 360 - angle, 0, 0.72); + } + x += offset * xdir; + y -= offset * ydir; + } + } + + for (QLineF line : primitives.m_lines) + { + QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle); + QLineF l = t.map(line); + Createdxf::drawLine(file_path, l, 0); + } + + for (QRectF rect : primitives.m_rectangles) + { + QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle); + QRectF r = t.mapRect(rect); + Createdxf::drawRectangle(file_path,r,0); + } + + for (QRectF circle_rect : primitives.m_circles) + { + QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle); + QPointF c = t.map(QPointF(circle_rect.center().x(),circle_rect.center().y())); + Createdxf::drawCircle(file_path,c,circle_rect.width()/2,0); + } + + for (QVector<QPointF> polygon : primitives.m_polygons) + { + if (polygon.size() == 0) + continue; + QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle); + QPolygonF poly = t.map(polygon); + if(poly.isClosed()) + Createdxf::drawPolygon(file_path,poly,0); + else + Createdxf::drawPolyline(file_path,poly,0); + } + + // Draw arcs and ellipses + for (QVector<qreal> arc : primitives.m_arcs) + { + if (arc.size() == 0) + continue; + qreal x = (elem_pos_x + arc.at(0)); + qreal y = (elem_pos_y + arc.at(1)); + qreal w = arc.at(2); + qreal h = arc.at(3); + qreal startAngle = arc.at(4); + qreal spanAngle = arc .at(5); + QRectF r(x,y,w,h); + QPointF hotspot(elem_pos_x,elem_pos_y); + Createdxf::drawArcEllipse(file_path, r, startAngle, spanAngle, hotspot, rotation_angle, 0); + } + if (properties.draw_terminals) { + // Draw terminals + QList<Terminal *> list_terminals = elmt->terminals(); + QColor col("red"); + QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle); + foreach(Terminal *tp, list_terminals) { + QPointF c = t.map(QPointF(tp->dock_elmt_.x(),tp->dock_elmt_.y())); + Createdxf::drawCircle(file_path,c,3.0,Createdxf::dxfColor(col)); + } + } + } + + //Draw conductors + foreach(Conductor *cond, list_conductors) { + QPolygonF poly; + bool firstseg = true; + foreach(ConductorSegment *segment, cond -> segmentsList()) { + //Createdxf::drawLine(file_path,QLineF(cond->pos()+segment->firstPoint(),cond->pos()+segment->secondPoint()),0); + if(firstseg){ + poly << cond->pos()+segment->firstPoint(); + firstseg = false; + } + poly << cond->pos()+segment->secondPoint(); + } + Createdxf::drawPolyline(file_path,poly,0); + //Draw conductor text item + ConductorTextItem *textItem = cond -> textItem(); + + if (textItem) { + qreal fontSize = textItem -> font().pointSizeF(); + if (fontSize < 0) + fontSize = textItem -> font().pixelSize(); + qreal angle = textItem -> rotation(); + qreal angler = angle * M_PI/180; + int xdir = -sin(angler); + int ydir = -cos(angler); + + qreal x = (cond->pos().x() + textItem -> pos().x()) + + xdir * fontSize * 1.8 + - ydir * fontSize; + qreal y = (cond->pos().y() + textItem -> pos().y()) + - ydir * fontSize * 1.8 + - xdir * fontSize * 0.9; + QStringList lines = textItem->toPlainText().split('\n'); + qreal offset = fontSize * 1.6; + foreach (QString line, lines) { + if (line.size() > 0 && line != "_" ) + Createdxf::drawText(file_path, line, QPointF(x, y), fontSize, 360-angle, 0, 0.72 ); + x += offset * xdir; + y -= offset * ydir; + } + } + + // Draw the junctions + QList<QPointF> junctions_list = cond->junctions(); + if (!junctions_list.isEmpty()) { + foreach(QPointF point, junctions_list) { + Createdxf::drawEllipse(file_path,QRectF(cond->pos().x() + point.x() - 1.5, cond->pos().y() + point.y() - 1.5, 3.0, 3.0),0); + } + } + } + + //Draw text items + foreach(DiagramTextItem *dti, list_texts) { + qreal fontSize = dti -> font().pointSizeF(); + if (fontSize < 0) + fontSize = dti -> font().pixelSize(); + + qreal angle = dti -> rotation(); + + QGraphicsItem *parent = dti->parentItem(); + while (parent) { + angle += parent->rotation(); + parent = parent->parentItem(); + } + + qreal angler = angle * M_PI/180; + int xdir = -sin(angler); + int ydir = -cos(angler); + qreal x = (dti->scenePos().x()) + + xdir * fontSize * 1.8 + - ydir * fontSize; + qreal y = dti->scenePos().y() + - ydir * fontSize * 1.8 + - xdir * fontSize * 0.9; + QStringList lines = dti -> toPlainText().split('\n'); + qreal offset = fontSize * 1.6; + foreach (QString line, lines) { + if (line.size() > 0 && line != "_" ) + Createdxf::drawText(file_path, line, QPointF(x, y), fontSize, 360-angle, Createdxf::dxfColor(dti->color()), 0.72 ); + x += offset * xdir; + y -= offset * ydir; + } + } + + //Draw the slave cross-reference labels + for (QGraphicsTextItem *xref : std::as_const(list_xref_texts)) + { + qreal fontSize = xref->font().pointSizeF(); + if (fontSize < 0) + fontSize = xref->font().pixelSize(); + + qreal angle = xref->rotation(); + QGraphicsItem *parent = xref->parentItem(); + while (parent) { + angle += parent->rotation(); + parent = parent->parentItem(); + } + + qreal angler = angle * M_PI/180; + int xdir = -sin(angler); + int ydir = -cos(angler); + qreal x = xref->scenePos().x() + + xdir * fontSize * 1.8 + - ydir * fontSize; + qreal y = xref->scenePos().y() + - ydir * fontSize * 1.8 + - xdir * fontSize * 0.9; + + const QStringList lines = xref->toPlainText().split('\n'); + const qreal offset = fontSize * 1.6; + for (const QString &line : lines) { + if (line.size() > 0 && line != QLatin1String("_")) { + Createdxf::drawText(file_path, line, QPointF(x, y), fontSize, + 360-angle, Createdxf::dxfColor(xref->defaultTextColor()), 0.72); + } + x += offset * xdir; + y -= offset * ydir; + } + } + + //Draw the master-side cross-reference items (table/cross), replaying + //their existing paint() unmodified through DxfPaintEngine instead of + //hand-porting drawAsCross()/drawAsContacts()/drawAsPlcTable(). + for (CrossRefItem *xref : std::as_const(list_master_xrefs)) + { + DxfPaintDevice dxf_device(file_path); + QPainter painter(&dxf_device); + painter.setWorldTransform(xref->sceneTransform()); + xref->paintForExport(&painter); + painter.end(); + } + + //Draw images -- collected above (list_images) but never actually + //drawn until now, an existing gap this reuses the same paint() + //-replay approach to fix: DiagramImageItem::paint() has no + //viewport-dependent logic (unlike CrossRefItem, which needs its own + //paintForExport() for that reason), so it's called directly with a + //default QStyleOptionGraphicsItem rather than needing an export- + //specific variant of its own. DxfPaintEngine::drawPixmap() is what + //actually turns the drawPixmap() call inside paint() into a + //placeholder outline, since this DXF dialect has no raster image + //entity to draw instead. + for (DiagramImageItem *image : std::as_const(list_images)) + { + DxfPaintDevice dxf_device(file_path); + QPainter painter(&dxf_device); + painter.setWorldTransform(image->sceneTransform()); + image->paintForExport(&painter); + painter.end(); + } + + Createdxf::dxfEnd(file_path); + + diagram -> applyProperties(previous); +} + +QPointF DxfExport::rotation_transformed(qreal px, + qreal py, + qreal origin_x, + qreal origin_y, + qreal angle) { + + angle *= -3.14159265 / 180; + + float s = sin(angle); + float c = cos(angle); + + // Vector to rotate: + qreal Vx = px - origin_x; + qreal Vy = py - origin_y; + + // rotate vector + float xnew = Vx * c - Vy * s; + float ynew = Vx * s + Vy * c; + + return QPointF(xnew + origin_x, ynew + origin_y); +} diff --git a/sources/dxfexport.h b/sources/dxfexport.h new file mode 100644 index 000000000..e1c98e5bb --- /dev/null +++ b/sources/dxfexport.h @@ -0,0 +1,42 @@ +/* + 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 <http://www.gnu.org/licenses/>. +*/ +#ifndef DXFEXPORT_H +#define DXFEXPORT_H + +#include <QPointF> +#include <QSize> +#include <QString> + +class Diagram; +class ExportProperties; + +/** + Writes a folio as a DXF file. Used by the export dialog and by + --export-dxf (discussion #1072), so both write the same file for the + same options. +*/ +namespace DxfExport +{ + QSize folioSize(Diagram *diagram, const ExportProperties &properties); + void write(Diagram *diagram, int width, int height, + const QString &path, const ExportProperties &properties); + QPointF rotation_transformed(qreal px, qreal py, + qreal origin_x, qreal origin_y, qreal angle); +} + +#endif // DXFEXPORT_H diff --git a/sources/exportdialog.cpp b/sources/exportdialog.cpp index 19a3b7f20..95d0f8fb6 100644 --- a/sources/exportdialog.cpp +++ b/sources/exportdialog.cpp @@ -19,6 +19,7 @@ #include "conductorsegment.h" #include "createdxf.h" +#include "dxfexport.h" #include "exportpropertieswidget.h" #include "factory/elementpicturefactory.h" #include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h" @@ -37,7 +38,6 @@ #include "qetmessagebox.h" #include <QGraphicsSimpleTextItem> -#include <QSet> #include <QSvgGenerator> #include <QtXml> #include <cmath> @@ -431,382 +431,6 @@ void ExportDialog::generateSvg( saveReloadDiagramParameters(diagram, false); } -/** - Exporte le schema en DXF - @param diagram Schema a exporter en DXF - @param width Largeur de l'export DXF - @param height Hauteur de l'export DXF - @param file_path -*/ -void ExportDialog::generateDxf( - Diagram *diagram, - int width, - int height, - QString &file_path) -{ - saveReloadDiagramParameters(diagram, true); - - width -= 2*Diagram::margin; - height -= 2*Diagram::margin; - - Createdxf::xScale = Createdxf::sheetWidth / double(width); - Createdxf::yScale = Createdxf::sheetHeight / double(height); - - Createdxf::dxfBegin(file_path); - - //Add project elements (lines, rectangles, circles, texts) to dxf file - if (epw -> exportProperties().draw_border) { - QRectF rect(Diagram::margin,Diagram::margin,width,height); - Createdxf::drawRectangle(file_path,rect,0); - } - diagram -> border_and_titleblock.drawDxf(file_path, 0); - - // Build the lists of elements. - QList<Element *> list_elements; - QList<Conductor *> list_conductors; - QList<DiagramTextItem *> list_texts; - QList<DiagramImageItem *> list_images; - //Slave cross-reference labels. They hang off a DynamicElementTextItem - //as plain QGraphicsTextItem children, so neither cast below picks them - //up and they were missing from the DXF entirely. - QList<QGraphicsTextItem *> list_xref_texts; - //Master-side cross-reference item (the table/cross drawn next to a - //report/master element). It paints itself with hand-written - //QPainter code across three modes (drawAsCross/drawAsContacts/ - //drawAsPlcTable), so instead of hand-porting each one it's replayed - //through DxfPaintEngine, which reuses paint() unmodified. - QList<CrossRefItem *> list_master_xrefs; - QList<QLineF *> list_lines; - QList<QRectF *> list_rectangles; - //QList<QRectF *> list_ellipses; - QList <QetShapeItem *> list_shapes; - QList <QetGraphicsTableItem *> list_tables; -// QList <Terminal *> list_terminals; - - // Determine les elements a "XMLiser" - // In stacking order (z, then insertion order, which a load makes the - // file's order), not items() order: that follows memory addresses, so - // the same folio came out in a different order on every export, as - // saving did before bugtracker #343. A rect query gives stacking order - // even with NoIndex; anything it misses keeps its items() place after - // the rest. - QList<QGraphicsItem *> stacked_items = diagram -> items( - QRectF(-1e9, -1e9, 2e9, 2e9), Qt::IntersectsItemBoundingRect, - Qt::AscendingOrder); - { - const QSet<QGraphicsItem *> ranked(stacked_items.cbegin(), stacked_items.cend()); - for (QGraphicsItem *qgi : diagram -> items()) { - if (!ranked.contains(qgi)) { - stacked_items << qgi; - } - } - } - for (QGraphicsItem *qgi : std::as_const(stacked_items)) { - if (Element *elmt = qgraphicsitem_cast<Element *>(qgi)) { - list_elements << elmt; - } else if (Conductor *f = qgraphicsitem_cast<Conductor *>(qgi)) { - list_conductors << f; - } else if (IndependentTextItem *iti = qgraphicsitem_cast<IndependentTextItem *>(qgi)) { - list_texts << iti; - } else if (DiagramImageItem *dii = qgraphicsitem_cast<DiagramImageItem *>(qgi)) { - list_images << dii; - } else if (QetShapeItem *dii = qgraphicsitem_cast<QetShapeItem *>(qgi)) { - list_shapes << dii; - } else if (DynamicElementTextItem *deti = qgraphicsitem_cast<DynamicElementTextItem *>(qgi)) { - list_texts << deti; - if (QGraphicsTextItem *xref = deti->slaveXrefItem()) { - list_xref_texts << xref; - } - } else if (QetGraphicsTableItem *gti = qgraphicsitem_cast<QetGraphicsTableItem *>(qgi)) { - list_tables << gti; - } else if (CrossRefItem *xref = qgraphicsitem_cast<CrossRefItem *>(qgi)) { - list_master_xrefs << xref; - } - } - - // Draw shapes - foreach (QetShapeItem *qsi, list_shapes) qsi->toDXF(file_path, qsi->pen()); - - // Draw tables - foreach (QetGraphicsTableItem *gti, list_tables) { - gti->toDXF(file_path); - } - - //Draw elements - foreach(Element *elmt, list_elements) - { - double rotation_angle = elmt -> orientation() * 90; - - qreal elem_pos_x = elmt -> pos().x(); - qreal elem_pos_y = elmt -> pos().y();// - (diagram -> margin / 2); - - ElementPictureFactory::primitives primitives = ElementPictureFactory::instance()->getPrimitives(elmt->location()); - - for(QGraphicsSimpleTextItem *text : primitives.m_texts) - { - qreal fontSize = text->font().pointSizeF(); - if (fontSize < 0) - fontSize = text->font().pixelSize(); - - qreal x = elem_pos_x + text->pos().x(); - qreal y = elem_pos_y + text->pos().y(); - - qreal angle = text -> rotation() + rotation_angle; - qreal angler = angle * M_PI/180; - int xdir = -sin(angler); - int ydir = -cos(angler); - - QPointF transformed_point = rotation_transformed(x, y, elem_pos_x, elem_pos_y, -rotation_angle); - x = transformed_point.x() - ydir * fontSize * 0.5; - y = transformed_point.y() - xdir * fontSize * 0.5; - QStringList lines = text->text().split('\n'); - qreal offset = fontSize * 1.6; - for (QString line : lines) - { - if (line.size() > 0 && line != "_" ) { - Createdxf::drawText(file_path, line, QPointF(x, y), fontSize, 360 - angle, 0, 0.72); - } - x += offset * xdir; - y -= offset * ydir; - } - } - - for (QLineF line : primitives.m_lines) - { - QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle); - QLineF l = t.map(line); - Createdxf::drawLine(file_path, l, 0); - } - - for (QRectF rect : primitives.m_rectangles) - { - QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle); - QRectF r = t.mapRect(rect); - Createdxf::drawRectangle(file_path,r,0); - } - - for (QRectF circle_rect : primitives.m_circles) - { - QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle); - QPointF c = t.map(QPointF(circle_rect.center().x(),circle_rect.center().y())); - Createdxf::drawCircle(file_path,c,circle_rect.width()/2,0); - } - - for (QVector<QPointF> polygon : primitives.m_polygons) - { - if (polygon.size() == 0) - continue; - QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle); - QPolygonF poly = t.map(polygon); - if(poly.isClosed()) - Createdxf::drawPolygon(file_path,poly,0); - else - Createdxf::drawPolyline(file_path,poly,0); - } - - // Draw arcs and ellipses - for (QVector<qreal> arc : primitives.m_arcs) - { - if (arc.size() == 0) - continue; - qreal x = (elem_pos_x + arc.at(0)); - qreal y = (elem_pos_y + arc.at(1)); - qreal w = arc.at(2); - qreal h = arc.at(3); - qreal startAngle = arc.at(4); - qreal spanAngle = arc .at(5); - QRectF r(x,y,w,h); - QPointF hotspot(elem_pos_x,elem_pos_y); - Createdxf::drawArcEllipse(file_path, r, startAngle, spanAngle, hotspot, rotation_angle, 0); - } - if (epw -> exportProperties().draw_terminals) { - // Draw terminals - QList<Terminal *> list_terminals = elmt->terminals(); - QColor col("red"); - QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle); - foreach(Terminal *tp, list_terminals) { - QPointF c = t.map(QPointF(tp->dock_elmt_.x(),tp->dock_elmt_.y())); - Createdxf::drawCircle(file_path,c,3.0,Createdxf::dxfColor(col)); - } - } - } - - //Draw conductors - foreach(Conductor *cond, list_conductors) { - QPolygonF poly; - bool firstseg = true; - foreach(ConductorSegment *segment, cond -> segmentsList()) { - //Createdxf::drawLine(file_path,QLineF(cond->pos()+segment->firstPoint(),cond->pos()+segment->secondPoint()),0); - if(firstseg){ - poly << cond->pos()+segment->firstPoint(); - firstseg = false; - } - poly << cond->pos()+segment->secondPoint(); - } - Createdxf::drawPolyline(file_path,poly,0); - //Draw conductor text item - ConductorTextItem *textItem = cond -> textItem(); - - if (textItem) { - qreal fontSize = textItem -> font().pointSizeF(); - if (fontSize < 0) - fontSize = textItem -> font().pixelSize(); - qreal angle = textItem -> rotation(); - qreal angler = angle * M_PI/180; - int xdir = -sin(angler); - int ydir = -cos(angler); - - qreal x = (cond->pos().x() + textItem -> pos().x()) - + xdir * fontSize * 1.8 - - ydir * fontSize; - qreal y = (cond->pos().y() + textItem -> pos().y()) - - ydir * fontSize * 1.8 - - xdir * fontSize * 0.9; - QStringList lines = textItem->toPlainText().split('\n'); - qreal offset = fontSize * 1.6; - foreach (QString line, lines) { - if (line.size() > 0 && line != "_" ) - Createdxf::drawText(file_path, line, QPointF(x, y), fontSize, 360-angle, 0, 0.72 ); - x += offset * xdir; - y -= offset * ydir; - } - } - - // Draw the junctions - QList<QPointF> junctions_list = cond->junctions(); - if (!junctions_list.isEmpty()) { - foreach(QPointF point, junctions_list) { - Createdxf::drawEllipse(file_path,QRectF(cond->pos().x() + point.x() - 1.5, cond->pos().y() + point.y() - 1.5, 3.0, 3.0),0); - } - } - } - - //Draw text items - foreach(DiagramTextItem *dti, list_texts) { - qreal fontSize = dti -> font().pointSizeF(); - if (fontSize < 0) - fontSize = dti -> font().pixelSize(); - - qreal angle = dti -> rotation(); - - QGraphicsItem *parent = dti->parentItem(); - while (parent) { - angle += parent->rotation(); - parent = parent->parentItem(); - } - - qreal angler = angle * M_PI/180; - int xdir = -sin(angler); - int ydir = -cos(angler); - qreal x = (dti->scenePos().x()) - + xdir * fontSize * 1.8 - - ydir * fontSize; - qreal y = dti->scenePos().y() - - ydir * fontSize * 1.8 - - xdir * fontSize * 0.9; - QStringList lines = dti -> toPlainText().split('\n'); - qreal offset = fontSize * 1.6; - foreach (QString line, lines) { - if (line.size() > 0 && line != "_" ) - Createdxf::drawText(file_path, line, QPointF(x, y), fontSize, 360-angle, Createdxf::dxfColor(dti->color()), 0.72 ); - x += offset * xdir; - y -= offset * ydir; - } - } - - //Draw the slave cross-reference labels - for (QGraphicsTextItem *xref : std::as_const(list_xref_texts)) - { - qreal fontSize = xref->font().pointSizeF(); - if (fontSize < 0) - fontSize = xref->font().pixelSize(); - - qreal angle = xref->rotation(); - QGraphicsItem *parent = xref->parentItem(); - while (parent) { - angle += parent->rotation(); - parent = parent->parentItem(); - } - - qreal angler = angle * M_PI/180; - int xdir = -sin(angler); - int ydir = -cos(angler); - qreal x = xref->scenePos().x() - + xdir * fontSize * 1.8 - - ydir * fontSize; - qreal y = xref->scenePos().y() - - ydir * fontSize * 1.8 - - xdir * fontSize * 0.9; - - const QStringList lines = xref->toPlainText().split('\n'); - const qreal offset = fontSize * 1.6; - for (const QString &line : lines) { - if (line.size() > 0 && line != QLatin1String("_")) { - Createdxf::drawText(file_path, line, QPointF(x, y), fontSize, - 360-angle, Createdxf::dxfColor(xref->defaultTextColor()), 0.72); - } - x += offset * xdir; - y -= offset * ydir; - } - } - - //Draw the master-side cross-reference items (table/cross), replaying - //their existing paint() unmodified through DxfPaintEngine instead of - //hand-porting drawAsCross()/drawAsContacts()/drawAsPlcTable(). - for (CrossRefItem *xref : std::as_const(list_master_xrefs)) - { - DxfPaintDevice dxf_device(file_path); - QPainter painter(&dxf_device); - painter.setWorldTransform(xref->sceneTransform()); - xref->paintForExport(&painter); - painter.end(); - } - - //Draw images -- collected above (list_images) but never actually - //drawn until now, an existing gap this reuses the same paint() - //-replay approach to fix: DiagramImageItem::paint() has no - //viewport-dependent logic (unlike CrossRefItem, which needs its own - //paintForExport() for that reason), so it's called directly with a - //default QStyleOptionGraphicsItem rather than needing an export- - //specific variant of its own. DxfPaintEngine::drawPixmap() is what - //actually turns the drawPixmap() call inside paint() into a - //placeholder outline, since this DXF dialect has no raster image - //entity to draw instead. - for (DiagramImageItem *image : std::as_const(list_images)) - { - DxfPaintDevice dxf_device(file_path); - QPainter painter(&dxf_device); - painter.setWorldTransform(image->sceneTransform()); - image->paintForExport(&painter); - painter.end(); - } - - Createdxf::dxfEnd(file_path); - - saveReloadDiagramParameters(diagram, false); -} - -QPointF ExportDialog::rotation_transformed(qreal px, - qreal py, - qreal origin_x, - qreal origin_y, - qreal angle) { - - angle *= -3.14159265 / 180; - - float s = sin(angle); - float c = cos(angle); - - // Vector to rotate: - qreal Vx = px - origin_x; - qreal Vy = py - origin_y; - - // rotate vector - float xnew = Vx * c - Vy * s; - float ynew = Vx * s + Vy * c; - - return QPointF(xnew + origin_x, ynew + origin_y); -} /** Slot effectuant les exports apres la validation du dialogue. @@ -952,11 +576,12 @@ void ExportDialog::exportDiagram(ExportDiagramLine *diagram_line) { target_file ); } else if (format_acronym == "DXF") { - generateDxf( + DxfExport::write( diagram_line -> diagram, diagram_line -> width -> value(), diagram_line -> height -> value(), - diagram_path + diagram_path, + export_properties ); } else { QImage image = generateImage( diff --git a/sources/exportdialog.h b/sources/exportdialog.h index 60206687b..095cb927d 100644 --- a/sources/exportdialog.h +++ b/sources/exportdialog.h @@ -37,7 +37,6 @@ class ExportDialog : public QDialog { // methods int diagramsToExportCount() const; - static QPointF rotation_transformed(qreal, qreal, qreal, qreal, qreal); private: ExportDialog(const ExportDialog &); @@ -91,7 +90,6 @@ class ExportDialog : public QDialog { QWidget *initDiagramsListPart(); void saveReloadDiagramParameters(Diagram *, bool = true); void generateSvg(Diagram *, int, int, bool, QIODevice &); - void generateDxf(Diagram *, int, int, QString &); QImage generateImage(Diagram *, int, int, bool); void exportDiagram(ExportDiagramLine *); qreal diagramRatio(Diagram *); diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index 4b3dfc760..04a696d68 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -207,6 +207,13 @@ bool QetScriptApi::exportSvg(const QString &outDir, bool showTerminals) return runFlag(QStringLiteral("--export-svg"), args); } +bool QetScriptApi::exportDxf(const QString &outDir, bool showTerminals) +{ + QStringList args{outDir}; + if (showTerminals) args << QStringLiteral("--show-terminals"); + return runFlag(QStringLiteral("--export-dxf"), args); +} + bool QetScriptApi::exportCables(const QString &output) { return runFlag(QStringLiteral("--export-cables"), {output}); diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index a95ddd2a0..14f1b4459 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -360,6 +360,7 @@ class QetScriptApi : public QObject Q_INVOKABLE bool exportPdf(const QString &output, bool showTerminals = false); Q_INVOKABLE bool exportPng(const QString &outDir, bool showTerminals = false); Q_INVOKABLE bool exportSvg(const QString &outDir, bool showTerminals = false); + Q_INVOKABLE bool exportDxf(const QString &outDir, bool showTerminals = false); Q_INVOKABLE bool exportCables(const QString &output); Q_INVOKABLE bool exportWires(const QString &output); Q_INVOKABLE bool exportBom(const QString &output); From f62a44827bfb032498f3ec8a8413400a1aa40636 Mon Sep 17 00:00:00 2001 From: ispyisail <shane.ringrose@gmail.com> Date: Mon, 28 Sep 2026 05:43:11 +1300 Subject: [PATCH 3/6] Add a setting to show collection search results as a tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1051 replaced the filtered tree with a ranked flat list. A new checkbox in Settings > General, "Afficher les résultats de recherche sous forme de liste triée" (elementscollection/search-flat-list, default on), keeps the ranked list; unticked restores the pre-#1051 filtered tree search. The Insert picker and shortcut bar call rankedSearch() directly and are unaffected. Down/Enter from the search field only apply to the flat list. Requested by scorpio810 on #1051. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG --- .../elementscollectionwidget.cpp | 27 ++++++++++++++++++- .../configpage/generalconfigurationpage.cpp | 2 ++ .../ui/configpage/generalconfigurationpage.ui | 10 +++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/sources/ElementsCollection/elementscollectionwidget.cpp b/sources/ElementsCollection/elementscollectionwidget.cpp index 198e6ddb1..222425243 100644 --- a/sources/ElementsCollection/elementscollectionwidget.cpp +++ b/sources/ElementsCollection/elementscollectionwidget.cpp @@ -1103,7 +1103,32 @@ void ElementsCollectionWidget::search() return; } - showFlatResults(rankedSearch(text, m_showed_index)); + QSettings settings; + if (settings.value(QStringLiteral("elementscollection/search-flat-list"), + true).toBool()) + { + showFlatResults(rankedSearch(text, m_showed_index)); + return; + } + + //Tree search: hide what does not match, expand what does. + clearFlatResults(); + hideCollection(true); + const QStringList text_list = text.split("+", Qt::SkipEmptyParts); + QModelIndexList match_index; + for (const QString &txt : text_list) { + match_index << m_model->match(m_showed_index.isValid() + ? m_model->index(0,0,m_showed_index) + : m_model->index(0,0), + Qt::UserRole+1, + QVariant(txt), + -1, + Qt::MatchContains + | Qt::MatchRecursive); + } + + for (const QModelIndex &index : std::as_const(match_index)) + showAndExpandItem(index); } /** diff --git a/sources/ui/configpage/generalconfigurationpage.cpp b/sources/ui/configpage/generalconfigurationpage.cpp index 41fa915bd..517548ad3 100644 --- a/sources/ui/configpage/generalconfigurationpage.cpp +++ b/sources/ui/configpage/generalconfigurationpage.cpp @@ -70,6 +70,7 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) : //Stored as "inserts" but presented as "edits", so the default (insert) //is the unchecked state -- a preference reads better as an opt-out. ui->m_collection_dblclick_edits->setChecked(!settings.value("elementscollection/double-click-inserts", true).toBool()); + ui->m_collection_search_flat_cb->setChecked(settings.value("elementscollection/search-flat-list", true).toBool()); ui->m_context_toolbar_cb->setChecked(settings.value("diagrameditor/context_toolbar", true).toBool()); ui->m_mouse_gestures_cb->setChecked(settings.value("diagrameditor/mouse_gestures", true).toBool()); ui->DiagramEditor_xGrid_sb->setValue(settings.value("diagrameditor/Xgrid", 10).toInt()); @@ -302,6 +303,7 @@ void GeneralConfigurationPage::applyConf() settings.setValue("diagrameditor/grid_display_startup", ui->grid_startup_cb->isChecked()); settings.setValue("diagrameditor/guides_display_startup", ui->guides_startup_cb->isChecked()); settings.setValue("elementscollection/double-click-inserts", !ui->m_collection_dblclick_edits->isChecked()); + settings.setValue("elementscollection/search-flat-list", ui->m_collection_search_flat_cb->isChecked()); settings.setValue("diagrameditor/context_toolbar", ui->m_context_toolbar_cb->isChecked()); settings.setValue("diagrameditor/mouse_gestures", ui->m_mouse_gestures_cb->isChecked()); //Grid step and key navigation diff --git a/sources/ui/configpage/generalconfigurationpage.ui b/sources/ui/configpage/generalconfigurationpage.ui index cfd57dd5c..027b73704 100644 --- a/sources/ui/configpage/generalconfigurationpage.ui +++ b/sources/ui/configpage/generalconfigurationpage.ui @@ -94,6 +94,16 @@ </property> </widget> </item> + <item> + <widget class="QCheckBox" name="m_collection_search_flat_cb"> + <property name="toolTip"> + <string>Coché : une seule liste, la meilleure correspondance en premier. Décoché : l'arborescence de la collection, filtrée sur la recherche.</string> + </property> + <property name="text"> + <string>Afficher les résultats de recherche sous forme de liste triée</string> + </property> + </widget> + </item> <item> <widget class="QCheckBox" name="m_context_toolbar_cb"> <property name="toolTip"> From adcf11270c4e25bfa38cffead6c0ff8267c2770a Mon Sep 17 00:00:00 2001 From: ispyisail <shane.ringrose@gmail.com> Date: Mon, 28 Sep 2026 05:55:20 +1300 Subject: [PATCH 4/6] Upgrade old element type names in saved query presets too A query saved to nomenclature.json (Load/Save in the query editor, since 2020) before June 2022 still says element_type = 'Simple'. Loading it ticked none of the type boxes and kept the old query, so the new table came out empty. Pass it through LegacyElementTypes::upgradeQuery() as ProjectDBModel::fromXml() now does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- sources/dataBase/ui/elementquerywidget.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sources/dataBase/ui/elementquerywidget.cpp b/sources/dataBase/ui/elementquerywidget.cpp index f5a42beb7..6331efb63 100644 --- a/sources/dataBase/ui/elementquerywidget.cpp +++ b/sources/dataBase/ui/elementquerywidget.cpp @@ -20,6 +20,7 @@ #include "../../properties/elementdata.h" #include "../../qetapp.h" #include "../../qetinformation.h" +#include "../legacyelementtypes.h" #include "../projectdatabase.h" #include "ui_elementquerywidget.h" @@ -674,7 +675,9 @@ void ElementQueryWidget::on_m_load_pb_clicked() auto value_object = value.toObject(); if (value_object.value("query").isString()) { - setQuery(value_object.value("query").toString()); + //A query saved before June 2022 names element types the old way + setQuery(LegacyElementTypes::upgradeQuery( + value_object.value("query").toString())); } } From 239065af7d16e1816c117e7a697f9b11e8abef12 Mon Sep 17 00:00:00 2001 From: ispyisail <shane.ringrose@gmail.com> Date: Mon, 28 Sep 2026 06:16:17 +1300 Subject: [PATCH 5/6] Open the text orientation dialog at the texts' current angle The "Orienter les textes" dialog always opened at 0, so turning a text at 90 degrees by a little meant typing the angle in again. It now opens at the angle every selected text and text group shares, and at 0 as before when they differ. The shortcut bar showed only a command's short name as its tooltip, which for this one ("Choose texts orientation") does not say what it does. It now adds the command's status tip on a second line: "Rotate selected texts to a specific angle". Checked in the GUI on a folio holding one text at 90 degrees: master opens the dialog at 0.00, this at 90.00. Fixes #1082 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- sources/diagramcontexttoolbar.cpp | 7 ++++- sources/qetdiagrameditor.cpp | 2 +- sources/undocommand/rotatetextscommand.cpp | 36 ++++++++++++++++++++-- sources/undocommand/rotatetextscommand.h | 6 ++-- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/sources/diagramcontexttoolbar.cpp b/sources/diagramcontexttoolbar.cpp index c8e8ab3be..610b2eec7 100644 --- a/sources/diagramcontexttoolbar.cpp +++ b/sources/diagramcontexttoolbar.cpp @@ -23,6 +23,7 @@ #include <QHBoxLayout> #include <QMouseEvent> #include <QSettings> +#include <QStringBuilder> #include <QToolButton> namespace { @@ -92,7 +93,11 @@ void DiagramContextToolbar::showAt(const QPoint &viewport_pos, } else { button->setIcon(action->icon()); } - button->setToolTip(text); + //The status tip says what the command does, which a short + //name such as "Orienter les textes" does not + button->setToolTip(action->statusTip().isEmpty() + ? text + : text % QLatin1Char('\n') % action->statusTip()); button->setEnabled(action->isEnabled()); connect(button, &QToolButton::clicked, action, &QAction::trigger); m_layout->addWidget(button); diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index 1845df15f..7407c892e 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -2059,7 +2059,7 @@ void QETDiagramEditor::selectionGroupTriggered(QAction *action) //nothing to rotate. if (RotateTextsCommand::hasSelectedTexts(diagram)) { - qreal rotation = 0; + qreal rotation = RotateTextsCommand::currentRotation(diagram); if (RotateTextsCommand::askRotation(rotation)) diagram->undoStack().push(new RotateTextsCommand(diagram, rotation)); } diff --git a/sources/undocommand/rotatetextscommand.cpp b/sources/undocommand/rotatetextscommand.cpp index 704b7bcd8..9936721f1 100644 --- a/sources/undocommand/rotatetextscommand.cpp +++ b/sources/undocommand/rotatetextscommand.cpp @@ -25,6 +25,8 @@ #include "../qetgraphicsitem/elementtextitemgroup.h" #include "../qtextorientationspinboxwidget.h" +#include <cmath> + /** @brief RotateTextsCommand::hasSelectedTexts @param diagram @@ -41,6 +43,35 @@ bool RotateTextsCommand::hasSelectedTexts(Diagram *diagram) return (!dc.selectedTexts().isEmpty() || !dc.selectedTextsGroup().isEmpty()); } +/** + @brief RotateTextsCommand::currentRotation + @param diagram + @return the rotation shared by every selected text and text group of + @p diagram, so the dialog can open at the angle they already have. + 0 when nothing is selected or when their angles differ. +*/ +qreal RotateTextsCommand::currentRotation(Diagram *diagram) +{ + if(!diagram) + return 0; + + DiagramContent dc(diagram); + QList<qreal> angles; + for(DiagramTextItem *dti : dc.selectedTexts()) + angles << dti->rotation(); + for(ElementTextItemGroup *etig : dc.selectedTextsGroup()) + angles << etig->rotation(); + + if(angles.isEmpty()) + return 0; + for(qreal angle : angles) + if(qAbs(angle - angles.first()) > 0.01) + return 0; + + //The dialog's spin box accepts -360 to 360 + return std::fmod(angles.first(), 360); +} + /** @brief RotateTextsCommand::RotateTextsCommand @param diagram : Apply the rotation to the selected texts and group of texts @@ -124,8 +155,8 @@ void RotateTextsCommand::redo() /** @brief RotateTextsCommand::askRotation Ask the user for an orientation. - @param rotation : set to the chosen angle when the dialog is accepted, - left untouched otherwise. + @param rotation : the angle the dialog opens at; set to the chosen + angle when the dialog is accepted, left untouched otherwise. @return true if the user accepted, false if they cancelled. Deliberately static and separate from the command: a QUndoCommand that @@ -145,6 +176,7 @@ bool RotateTextsCommand::askRotation(qreal &rotation) QTextOrientationSpinBoxWidget *ori_widget = QETApp::createTextOrientationSpinBoxWidget(); ori_widget->setParent(&ori_text_dialog); + ori_widget->setOrientation(rotation); ori_widget->spinBox()->selectAll(); QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); diff --git a/sources/undocommand/rotatetextscommand.h b/sources/undocommand/rotatetextscommand.h index 0ddf58830..ebe7f3bf6 100644 --- a/sources/undocommand/rotatetextscommand.h +++ b/sources/undocommand/rotatetextscommand.h @@ -40,7 +40,7 @@ class QParallelAnimationGroup; Typical interactive use: @code if (RotateTextsCommand::hasSelectedTexts(diagram)) { - qreal rotation = 0; + qreal rotation = RotateTextsCommand::currentRotation(diagram); if (RotateTextsCommand::askRotation(rotation)) diagram->undoStack().push(new RotateTextsCommand(diagram, rotation)); } @@ -53,7 +53,9 @@ class RotateTextsCommand : public QUndoCommand /// @return true if @p diagram has at least one selected text or text group to rotate. static bool hasSelectedTexts(Diagram *diagram); - /// Open the orientation dialog. @return true and set @p rotation if accepted, false if cancelled. + /// @return the angle the selected texts share, or 0 if they differ. + static qreal currentRotation(Diagram *diagram); + /// Open the orientation dialog at @p rotation. @return true and set @p rotation if accepted, false if cancelled. static bool askRotation(qreal &rotation); void undo() override; From 83bb7e14e07cb96cb07fa55f27800328dd5c7ed4 Mon Sep 17 00:00:00 2001 From: ispyisail <shane.ringrose@gmail.com> Date: Mon, 28 Sep 2026 06:23:33 +1300 Subject: [PATCH 6/6] Add the status tip to the S shortcut bar's tooltips too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit changed only the bar that appears beside the cursor after a click. The S shortcut bar builds its own tooltips, so its "Orienter les textes" button still said only that. Its tooltips now carry the status tip on a second line as well, after the keyboard shortcut. Checked in the GUI: the S bar's second button shows "Orienter les textes (Ctrl+Space)" over "Pivote les textes sélectionnés à un angle précis". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- sources/ElementsCollection/elementpickerpopup.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/sources/ElementsCollection/elementpickerpopup.cpp b/sources/ElementsCollection/elementpickerpopup.cpp index 2cfc1aaa4..5ebc14fd4 100644 --- a/sources/ElementsCollection/elementpickerpopup.cpp +++ b/sources/ElementsCollection/elementpickerpopup.cpp @@ -404,10 +404,16 @@ void ElementPickerPopup::setCommands(const QStringList &ids) button->setIcon(action->icon()); } const QKeySequence key = action->shortcut(); - button->setToolTip(key.isEmpty() - ? text - : QStringLiteral("%1 (%2)").arg( - text, key.toString(QKeySequence::NativeText))); + QString tip = key.isEmpty() + ? text + : QStringLiteral("%1 (%2)").arg( + text, key.toString(QKeySequence::NativeText)); + //The status tip says what the command does, which a short + //name such as "Orienter les textes" does not + if (!action->statusTip().isEmpty()) { + tip += QLatin1Char('\n') + action->statusTip(); + } + button->setToolTip(tip); button->setEnabled(action->isEnabled()); button->setFocusPolicy(Qt::NoFocus); connect(button, &QToolButton::clicked, this, [this, action]() {