From 4278ef864eb18a9cdf59b6a487df757b5ae7eae1 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 27 Sep 2026 19:03:17 +1300 Subject: [PATCH] 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);