diff --git a/sources/cli_export.cpp b/sources/cli_export.cpp index 6dc295196..ec4185a86 100644 --- a/sources/cli_export.cpp +++ b/sources/cli_export.cpp @@ -331,7 +331,8 @@ int exportImages(QETProject &project, const QString &format, /// 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) +int exportDxf(QETProject &project, const QString &out_dir, bool showTerminals, + bool dxfBlocks) { const QList diagrams = project.diagrams(); if (diagrams.isEmpty()) { @@ -344,6 +345,8 @@ int exportDxf(QETProject &project, const QString &out_dir, bool showTerminals) properties.format = QStringLiteral("DXF"); if (showTerminals) properties.draw_terminals = true; + if (dxfBlocks) + properties.dxf_blocks = true; int index = 0; bool has_images = false; @@ -982,6 +985,7 @@ int run(const QStringList &args) // collected below. QStringList filtered = args; const bool showTerminals = filtered.removeAll("--show-terminals") > 0; + const bool dxfBlocks = filtered.removeAll("--dxf-blocks") > 0; // --no-slaves and --no-junctions leave rows out of --export-bom. const bool includeSlaves = filtered.removeAll("--no-slaves") == 0; const bool includeJunctions = filtered.removeAll("--no-junctions") == 0; @@ -1038,7 +1042,7 @@ int run(const QStringList &args) if (format == "pdf") return exportPdf(project, output, showTerminals); if (format == "dxf") - return exportDxf(project, output, showTerminals); + return exportDxf(project, output, showTerminals, dxfBlocks); 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 e9323a035..1838fa347 100644 --- a/sources/cli_export.h +++ b/sources/cli_export.h @@ -45,7 +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-dxf [--show-terminals] [--dxf-blocks] qelectrotech --export-cables qelectrotech --export-wires qelectrotech --export-bom [--no-slaves] [--no-junctions] @@ -66,6 +66,8 @@ namespace CLIExport { does; off by default, matching the GUI export dialog's default. For DXF, draws the terminal markers. Has no effect on the list export modes. + --dxf-blocks: write each symbol as a DXF block, placed with one + INSERT per use, so a CAD program selects it as one object. 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 bf24c1add..bb4e3223c 100644 --- a/sources/createdxf.cpp +++ b/sources/createdxf.cpp @@ -68,7 +68,16 @@ Createdxf::~Createdxf() /* Header section of every DXF file. @param layers : the layers the entities will be written on, declared in the LAYER table beside layer "0", which every DXF has. */ -void Createdxf::dxfBegin (const QString& fileName, const QStringList &layers) +/** + @brief Createdxf::dxfBegin + Write the header, the tables with @a layers, and the BLOCKS section, + then open the ENTITIES section. @a writeBlocks, if given, is called + inside the BLOCKS section to write the block definitions + (dxfBlockBegin(), the entities, dxfBlockEnd()): a block has to be + defined before any entity, so before anything is drawn. +*/ +void Createdxf::dxfBegin (const QString& fileName, const QStringList &layers, + const std::function &writeBlocks) { layer = QStringLiteral("0"); @@ -276,17 +285,162 @@ void Createdxf::dxfBegin (const QString& fileName, const QStringList &layers) To_Dxf << "SECTION" << "\r\n"; To_Dxf << 2 << "\r\n"; To_Dxf << "BLOCKS" << "\r\n"; + file.close(); + } + if (writeBlocks) + writeBlocks(); + QFile entities(fileName); + if (entities.open(QFile::Append)) { + QTextStream To_Dxf(&entities); To_Dxf << 0 << "\r\n"; To_Dxf << "ENDSEC" << "\r\n"; To_Dxf << 0 << "\r\n"; To_Dxf << "SECTION" << "\r\n"; To_Dxf << 2 << "\r\n"; To_Dxf << "ENTITIES" << "\r\n"; - file.close(); + entities.close(); } } } +/** + @brief Createdxf::dxfBlockBegin + Start the definition of block @a name, with its base point at @a x, + @a y. Only inside dxfBegin()'s writeBlocks; every entity written until + dxfBlockEnd() belongs to the block. + @param hasAttributes : the block has attribute definitions +*/ +void Createdxf::dxfBlockBegin(const QString &fileName, + const QString &name, + double x, + double y, + bool hasAttributes) +{ + QFile file(fileName); + if (!file.open(QFile::Append)) + return; + QTextStream To_Dxf(&file); + To_Dxf << 0 << "\r\n"; + To_Dxf << "BLOCK" << "\r\n"; + To_Dxf << 8 << "\r\n"; + To_Dxf << "0" << "\r\n"; + To_Dxf << 2 << "\r\n"; + To_Dxf << name << "\r\n"; + To_Dxf << 70 << "\r\n"; + To_Dxf << (hasAttributes ? 2 : 0) << "\r\n"; + To_Dxf << 10 << "\r\n"; + To_Dxf << x << "\r\n"; + To_Dxf << 20 << "\r\n"; + To_Dxf << y << "\r\n"; + To_Dxf << 30 << "\r\n"; + To_Dxf << 0.0 << "\r\n"; + file.close(); +} + +/** + @brief Createdxf::dxfBlockEnd + End the block started by dxfBlockBegin(). +*/ +void Createdxf::dxfBlockEnd(const QString &fileName) +{ + QFile file(fileName); + if (!file.open(QFile::Append)) + return; + QTextStream To_Dxf(&file); + To_Dxf << 0 << "\r\n"; + To_Dxf << "ENDBLK" << "\r\n"; + To_Dxf << 8 << "\r\n"; + To_Dxf << "0" << "\r\n"; + file.close(); +} + +/** + @brief Createdxf::drawInsert + Place block @a name at @a x, @a y, turned counter-clockwise by + @a rotation degrees, on the current layer, followed by its + @a attributes. +*/ +void Createdxf::drawInsert(const QString &fileName, + const QString &name, + double x, + double y, + double rotation, + const QList &attributes) +{ + QFile file(fileName); + if (!file.open(QFile::Append)) + return; + QTextStream To_Dxf(&file); + To_Dxf << 0 << "\r\n"; + To_Dxf << "INSERT" << "\r\n"; + To_Dxf << 8 << "\r\n"; + To_Dxf << layer << "\r\n"; + if (!attributes.isEmpty()) { + To_Dxf << 66 << "\r\n"; + To_Dxf << 1 << "\r\n"; + } + To_Dxf << 2 << "\r\n"; + To_Dxf << name << "\r\n"; + To_Dxf << 10 << "\r\n"; + To_Dxf << x << "\r\n"; + To_Dxf << 20 << "\r\n"; + To_Dxf << y << "\r\n"; + To_Dxf << 30 << "\r\n"; + To_Dxf << 0.0 << "\r\n"; + To_Dxf << 50 << "\r\n"; + To_Dxf << rotation << "\r\n"; + for (const Attribute &attribute : attributes) { + To_Dxf << 0 << "\r\n"; + To_Dxf << "ATTRIB" << "\r\n"; + To_Dxf << 8 << "\r\n"; + To_Dxf << layer << "\r\n"; + To_Dxf << 62 << "\r\n"; + To_Dxf << entityColour(attribute.colour) << "\r\n"; + To_Dxf << 10 << "\r\n"; + To_Dxf << attribute.x << "\r\n"; + To_Dxf << 20 << "\r\n"; + To_Dxf << attribute.y << "\r\n"; + To_Dxf << 30 << "\r\n"; + To_Dxf << 0.0 << "\r\n"; + To_Dxf << 40 << "\r\n"; + To_Dxf << attribute.height << "\r\n"; + To_Dxf << 1 << "\r\n"; + To_Dxf << singleLine(attribute.text) << "\r\n"; + To_Dxf << 2 << "\r\n"; + To_Dxf << attribute.tag << "\r\n"; + To_Dxf << 70 << "\r\n"; + To_Dxf << (attribute.invisible ? 1 : 0) << "\r\n"; + To_Dxf << 50 << "\r\n"; + To_Dxf << attribute.rotation << "\r\n"; + To_Dxf << 41 << "\r\n"; + To_Dxf << attribute.xScaleW << "\r\n"; + } + if (!attributes.isEmpty()) { + To_Dxf << 0 << "\r\n"; + To_Dxf << "SEQEND" << "\r\n"; + To_Dxf << 8 << "\r\n"; + To_Dxf << layer << "\r\n"; + } + file.close(); +} + +/** + @brief Createdxf::blockName + @return @a name as a block or attribute name an R10 reader accepts: + upper case, only A-Z, 0-9, _, - and $, at most 31 characters +*/ +QString Createdxf::blockName(const QString &name) +{ + QString result; + for (const QChar c : name.toUpper()) { + const ushort u = c.unicode(); + const bool ok = (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9') + || u == '_' || u == '-' || u == '$'; + result += ok ? c : QChar('_'); + } + return result.left(31); +} + /** @brief Createdxf::dxfEnd End Section of every DXF File diff --git a/sources/createdxf.h b/sources/createdxf.h index 5e4a18d23..87d15c6b9 100644 --- a/sources/createdxf.h +++ b/sources/createdxf.h @@ -22,6 +22,8 @@ #include #include +#include + /** @brief The Createdxf class This class exports the project to DXF Format @@ -31,8 +33,35 @@ class Createdxf public: Createdxf(); ~Createdxf(); - static void dxfBegin (const QString&, const QStringList &layers = QStringList()); + static void dxfBegin (const QString&, const QStringList &layers = QStringList(), + const std::function &writeBlocks = {}); static void dxfEnd(const QString&); + static void dxfBlockBegin(const QString &fileName, + const QString &name, + double x, + double y, + bool hasAttributes = false); + static void dxfBlockEnd(const QString &fileName); + /// One attribute of a placed block, in DXF units + struct Attribute + { + QString tag; + QString text; + double x = 0; + double y = 0; + double height = 0; + double rotation = 0; + double xScaleW = 1; + int colour = 0; + bool invisible = false; + }; + static void drawInsert(const QString &fileName, + const QString &name, + double x, + double y, + double rotation, + const QList &attributes = {}); + static QString blockName(const QString &name); // you can add more functions to create more drawings. static void drawCircle( const QString&, diff --git a/sources/dxfexport.cpp b/sources/dxfexport.cpp index 3e587660c..c04cdc71c 100644 --- a/sources/dxfexport.cpp +++ b/sources/dxfexport.cpp @@ -42,6 +42,125 @@ #include #include +namespace { + /** + Draw the drawing of @a elmt's definition: its static texts, lines, + rectangles, circles, polygons and arcs, and its terminals when + @a draw_terminals, as if it were placed at @a elem_pos_x, + @a elem_pos_y and turned @a rotation_angle degrees. Used for every + symbol drawn in full, and once per block for a block's content. + */ + void drawSymbol(const QString &file_path, Element *elmt, + qreal elem_pos_x, qreal elem_pos_y, + double rotation_angle, bool draw_terminals) + { + using namespace DxfExport; + ElementPictureFactory::primitives primitives = ElementPictureFactory::instance()->getPrimitives(elmt->location()); + + Createdxf::layer = Layer::SymbolTexts; + 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 = DxfExport::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; + } + } + + Createdxf::layer = Layer::Symbols; + 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 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 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 (draw_terminals) { + Createdxf::layer = Layer::Terminals; + // Draw terminals + QList 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)); + } + } + } + + /// Nothing would be drawn for @a elmt (its definition is missing) + bool drawsNothing(Element *elmt, bool draw_terminals) + { + const ElementPictureFactory::primitives p = + ElementPictureFactory::instance()->getPrimitives(elmt->location()); + return p.m_lines.isEmpty() && p.m_rectangles.isEmpty() + && p.m_circles.isEmpty() && p.m_polygons.isEmpty() + && p.m_arcs.isEmpty() && p.m_texts.isEmpty() + && (!draw_terminals || elmt->terminals().isEmpty()); + } +} + /** @brief DxfExport::folioSize @return the size of @a diagram as exported with @a properties: its border @@ -88,19 +207,6 @@ void DxfExport::write(Diagram *diagram, int width, int height, Createdxf::xScale = scale; Createdxf::yScale = scale; - Createdxf::dxfBegin(file_path, Layer::all()); - - //Each kind of content on its own layer (discussion #1071). The - //border's title block switches to Layer::TitleBlock itself, see - //BorderTitleBlock::drawDxf(). - Createdxf::layer = Layer::Border; - //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 list_elements; QList list_conductors; @@ -169,6 +275,58 @@ void DxfExport::write(Diagram *diagram, int width, int height, } } + //Symbols as blocks: one block per symbol definition the folio + //uses, named after its file and numbered in the order first met, + //so a repeat export is identical. A block has to be written before + //any entity, so they are all collected first. + QHash block_names; // location -> block name + QList block_models; // one placed symbol per block + if (properties.dxf_blocks) { + QSet used; + for (Element *elmt : std::as_const(list_elements)) { + const QString key = elmt -> location().toString(); + if (block_names.contains(key) + || drawsNothing(elmt, properties.draw_terminals)) + continue; + QString file_name = elmt -> location().fileName(); + if (file_name.endsWith(QLatin1String(".elmt"))) + file_name.chop(5); + const QString base = Createdxf::blockName( + QStringLiteral("QET_") + file_name); + QString name = base; + for (int i = 2 ; used.contains(name) ; ++i) { + const QString suffix = QStringLiteral("_%1").arg(i); + name = base.left(31 - suffix.size()) + suffix; + } + used << name; + block_names.insert(key, name); + block_models << elmt; + } + } + + //A block is drawn as if its symbol sat unturned at the folio's + //origin, which is DXF (0, sheetHeight); that is its base point. + Createdxf::dxfBegin(file_path, Layer::all(), [&]() { + for (Element *model : std::as_const(block_models)) { + Createdxf::dxfBlockBegin(file_path, + block_names.value(model -> location().toString()), + 0, Createdxf::sheetHeight); + drawSymbol(file_path, model, 0, 0, 0, properties.draw_terminals); + Createdxf::dxfBlockEnd(file_path); + } + }); + + //Each kind of content on its own layer (discussion #1071). The + //border's title block switches to Layer::TitleBlock itself, see + //BorderTitleBlock::drawDxf(). + Createdxf::layer = Layer::Border; + //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); + // Draw shapes Createdxf::layer = Layer::Shapes; foreach (QetShapeItem *qsi, list_shapes) qsi->toDXF(file_path, qsi->pen()); @@ -179,105 +337,24 @@ void DxfExport::write(Diagram *diagram, int width, int height, gti->toDXF(file_path); } - //Draw elements + //Draw elements: each one in full, or as an INSERT of its block foreach(Element *elmt, list_elements) { - double rotation_angle = elmt -> orientation() * 90; + const double rotation_angle = elmt -> orientation() * 90; + const qreal elem_pos_x = elmt -> pos().x(); + const qreal elem_pos_y = elmt -> pos().y(); - 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()); - - Createdxf::layer = Layer::SymbolTexts; - 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; - } - } - - Createdxf::layer = Layer::Symbols; - 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 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 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) { - Createdxf::layer = Layer::Terminals; - // Draw terminals - QList 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)); - } + const QString block = block_names.value(elmt -> location().toString()); + if (block.isEmpty()) { + drawSymbol(file_path, elmt, elem_pos_x, elem_pos_y, + rotation_angle, properties.draw_terminals); + } else { + Createdxf::layer = Layer::Symbols; + //QElectroTech turns a symbol clockwise, DXF counter-clockwise + Createdxf::drawInsert(file_path, block, + elem_pos_x * Createdxf::xScale, + Createdxf::sheetHeight - elem_pos_y * Createdxf::yScale, + std::fmod(360 - rotation_angle, 360)); } } diff --git a/sources/exportproperties.cpp b/sources/exportproperties.cpp index 7e85a4b2d..3f11f213e 100644 --- a/sources/exportproperties.cpp +++ b/sources/exportproperties.cpp @@ -37,6 +37,7 @@ ExportProperties::ExportProperties() : draw_terminal_names(false), draw_bg_transparent(false), draw_colored_conductors(true), + dxf_blocks(false), exported_area(QET::BorderArea) { } @@ -77,6 +78,8 @@ void ExportProperties::toSettings(QSettings &settings, draw_bg_transparent); settings.setValue(prefix % "drawcoloredconductors", draw_colored_conductors); + settings.setValue(prefix % "dxfblocks", + dxf_blocks); settings.setValue(prefix % "area", QET::diagramAreaToString(exported_area)); } @@ -115,6 +118,8 @@ void ExportProperties::fromSettings(QSettings &settings, draw_colored_conductors = settings.value( prefix % "drawcoloredconductors", true ).toBool(); + dxf_blocks = settings.value(prefix % "dxfblocks", + false).toBool(); exported_area = QET::diagramAreaFromString( settings.value( diff --git a/sources/exportproperties.h b/sources/exportproperties.h index af34f6868..c9489496a 100644 --- a/sources/exportproperties.h +++ b/sources/exportproperties.h @@ -50,6 +50,7 @@ class ExportProperties { bool draw_terminal_names; ///< Whether to render terminal names/labels bool draw_bg_transparent; ///< Whether to use transparency for SVG-Export bool draw_colored_conductors; ///< Whether to render conductors colors + bool dxf_blocks; ///< DXF: each symbol as a block, placed with an INSERT QET::DiagramArea exported_area; ///< Area of diagrams to be rendered }; #endif diff --git a/sources/exportpropertieswidget.cpp b/sources/exportpropertieswidget.cpp index 8eccc0805..e95a5cb6d 100644 --- a/sources/exportpropertieswidget.cpp +++ b/sources/exportpropertieswidget.cpp @@ -66,6 +66,7 @@ ExportProperties ExportPropertiesWidget::exportProperties() const export_properties.draw_terminal_names = draw_terminal_names -> isChecked(); export_properties.draw_bg_transparent = draw_bg_transparent -> isChecked(); export_properties.draw_colored_conductors = draw_colored_conductors -> isChecked(); + export_properties.dxf_blocks = dxf_blocks -> isChecked(); export_properties.exported_area = export_border -> isChecked() ? QET::BorderArea : QET::ElementsArea; return(export_properties); @@ -89,6 +90,7 @@ void ExportPropertiesWidget::setExportProperties(const ExportProperties &export_ draw_terminal_names -> setChecked(export_properties.draw_terminal_names); draw_bg_transparent -> setChecked(export_properties.draw_bg_transparent); draw_colored_conductors -> setChecked(export_properties.draw_colored_conductors); + dxf_blocks -> setChecked(export_properties.dxf_blocks); if (export_properties.exported_area == QET::BorderArea) { export_border -> setChecked(true); @@ -219,6 +221,11 @@ void ExportPropertiesWidget::build() // use transparent background for SVG-Export draw_bg_transparent = new QCheckBox(tr("SVG: fond transparent"), groupbox_options); optionshlayout -> addWidget(draw_bg_transparent, 4, 0); + + // each symbol as a DXF block (issue #1339) + dxf_blocks = new QCheckBox(tr("DXF : symboles en blocs"), groupbox_options); + dxf_blocks -> setToolTip(tr("Chaque symbole devient un bloc DXF, sélectionnable d'un clic dans un logiciel de CAO")); + optionshlayout -> addWidget(dxf_blocks, 4, 1); vboxLayout -> addWidget(groupbox_options); @@ -234,6 +241,7 @@ void ExportPropertiesWidget::build() setTabOrder(draw_titleblock, draw_terminals); setTabOrder(draw_terminals, draw_colored_conductors); setTabOrder(draw_colored_conductors, draw_bg_transparent); + setTabOrder(draw_bg_transparent, dxf_blocks); // connexion du bouton permettant le choix du repertoire connect(button_browse, &QPushButton::released, this, &ExportPropertiesWidget::slot_chooseADirectory); @@ -249,6 +257,7 @@ void ExportPropertiesWidget::build() connect(draw_terminal_names, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged); connect(draw_bg_transparent, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged); connect(draw_colored_conductors, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged); + connect(dxf_blocks, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged); #else connect(draw_grid, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged); connect(draw_border, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged); @@ -257,5 +266,6 @@ void ExportPropertiesWidget::build() connect(draw_terminal_names, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged); connect(draw_bg_transparent, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged); connect(draw_colored_conductors, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged); + connect(dxf_blocks, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged); #endif } diff --git a/sources/exportpropertieswidget.h b/sources/exportpropertieswidget.h index ae0c97acd..f00032c65 100644 --- a/sources/exportpropertieswidget.h +++ b/sources/exportpropertieswidget.h @@ -65,6 +65,7 @@ class ExportPropertiesWidget : public QWidget { QCheckBox *draw_terminal_names; QCheckBox *draw_bg_transparent; QCheckBox *draw_colored_conductors; + QCheckBox *dxf_blocks; QRadioButton *export_border; QRadioButton *export_elements; QButtonGroup *exported_content_choices; diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index dd8244a93..b44bc3c77 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -473,6 +473,17 @@ target_link_libraries(tst_dxfcurves PRIVATE Qt::Test) target_compile_definitions(tst_dxfcurves PRIVATE "QET_TEST_BINARY_PATH=\"$\"") +# --export-dxf --dxf-blocks: a symbol placed twice is one BLOCK and two +# INSERTs (issue #1339). +add_executable( + tst_dxfblocks + tst_dxfblocks.cpp) +add_test(NAME tst_dxfblocks COMMAND tst_dxfblocks) +add_dependencies(tst_dxfblocks qelectrotech) +target_link_libraries(tst_dxfblocks PRIVATE Qt::Test) +target_compile_definitions(tst_dxfblocks PRIVATE + "QET_TEST_BINARY_PATH=\"$\"") + # --export-pdf puts an A3 folio on an A3 page whichever way it is turned: # QPageSize matches standard sheets upright only, so a wide folio is matched # upright and turned. fixtures/pdf_page_a3.qet has one folio each way. diff --git a/tests/qttest/fixtures/dxf_blocks.qet b/tests/qttest/fixtures/dxf_blocks.qet new file mode 100644 index 000000000..ffd6f9c4f --- /dev/null +++ b/tests/qttest/fixtures/dxf_blocks.qet @@ -0,0 +1,62 @@ + + + 10/2/26 + 02-10-2026 + 2026-10-02 + dxf_blocks + 18:39 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Imported elements + + + + + + Circle and arc + + + + + + + + + + diff --git a/tests/qttest/tst_dxfblocks.cpp b/tests/qttest/tst_dxfblocks.cpp new file mode 100644 index 000000000..bc409c85d --- /dev/null +++ b/tests/qttest/tst_dxfblocks.cpp @@ -0,0 +1,96 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include +#include +#include + +/** + --export-dxf --dxf-blocks (issue #1339): a symbol placed twice is one + BLOCK and two INSERTs, the second turned a quarter; without the switch + there is neither. +*/ +class tst_dxfblocks : public QObject +{ + Q_OBJECT + + /// The group code / value pairs of the exported folio + static QList> exportPairs(const QStringList &extra) + { + const QString project = QFINDTESTDATA("fixtures/dxf_blocks.qet"); + QTemporaryDir dir; + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); + QProcess proc; + proc.setProcessEnvironment(env); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), + QStringList{QStringLiteral("--export-dxf"), project, dir.path()} + extra); + if (!proc.waitForFinished(60000) || proc.exitCode() != 0) + return {}; + QFile file(dir.filePath(QStringLiteral("01_diagram.dxf"))); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + return {}; + const QStringList values = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n')); + QList> pairs; + for (int i = 0 ; i + 1 < values.size() ; i += 2) + pairs << qMakePair(values.at(i).trimmed(), values.at(i + 1).trimmed()); + return pairs; + } + + /// The values of code @a code in each entity of type @a type + static QStringList values(const QList> &pairs, + const QString &type, const QString &code) + { + QStringList found; + for (int i = 0 ; i < pairs.size() ; ++i) { + if (pairs.at(i).first != QLatin1String("0") || pairs.at(i).second != type) + continue; + for (int j = i + 1 ; j < pairs.size() && pairs.at(j).first != QLatin1String("0") ; ++j) + if (pairs.at(j).first == code) + found << pairs.at(j).second; + } + return found; + } + +private slots: + void blocks() + { + const auto pairs = exportPairs({QStringLiteral("--dxf-blocks")}); + QVERIFY2(!pairs.isEmpty(), "--export-dxf --dxf-blocks failed"); + + QCOMPARE(values(pairs, QStringLiteral("BLOCK"), QStringLiteral("2")), + QStringList{QStringLiteral("QET_CURVES")}); + QCOMPARE(values(pairs, QStringLiteral("INSERT"), QStringLiteral("2")), + (QStringList{QStringLiteral("QET_CURVES"), QStringLiteral("QET_CURVES")})); + QCOMPARE(values(pairs, QStringLiteral("INSERT"), QStringLiteral("50")), + (QStringList{QStringLiteral("0"), QStringLiteral("270")})); + //The circle is drawn once, inside the block + QCOMPARE(values(pairs, QStringLiteral("CIRCLE"), QStringLiteral("8")).size(), 1); + } + + void flatWithoutSwitch() + { + const auto pairs = exportPairs({}); + QVERIFY2(!pairs.isEmpty(), "--export-dxf failed"); + QVERIFY(values(pairs, QStringLiteral("BLOCK"), QStringLiteral("2")).isEmpty()); + QVERIFY(values(pairs, QStringLiteral("INSERT"), QStringLiteral("2")).isEmpty()); + QCOMPARE(values(pairs, QStringLiteral("CIRCLE"), QStringLiteral("8")).size(), 2); + } +}; + +QTEST_APPLESS_MAIN(tst_dxfblocks) +#include "tst_dxfblocks.moc"