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 &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 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(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 [--show-terminals] qelectrotech --export-png [--show-terminals] qelectrotech --export-svg [--show-terminals] + qelectrotech --export-dxf [--show-terminals] qelectrotech --export-cables qelectrotech --export-wires qelectrotech --export-bom @@ -57,11 +58,14 @@ namespace CLIExport { qelectrotech --set-titleblock key=value... PDF: one multi-page document (one diagram per page). - PNG/SVG: one file per diagram, named /_.<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);