Add an option to export symbols as DXF blocks (#1339)

With "DXF : symboles en blocs" ticked in the export dialog, or
--dxf-blocks on the command line, each symbol definition a folio uses is
written once as a DXF block and every placed symbol as an INSERT of it.
A CAD program then selects, counts and replaces a symbol as one object.

The block is drawn by the same code as a symbol drawn in full, as if
the symbol sat unturned at the folio's origin, which is DXF (0,
sheetHeight): that is the block's base point, so the INSERT carries the
symbol's position and quarter-turn and nothing else. Blocks have to come
before any entity, so the export collects them first and dxfBegin()
writes them in its BLOCKS section. Entities in a block keep their own
layers. Off by default; with it off the file is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-07 09:50:18 +13:00
parent 83f72c95c6
commit c3ed2daca8
12 changed files with 567 additions and 115 deletions
+6 -2
View File
@@ -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<Diagram *> 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")
+3 -1
View File
@@ -45,7 +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-dxf <project.qet> <output_dir> [--show-terminals] [--dxf-blocks]
qelectrotech --export-cables <project.qet> <output.csv>
qelectrotech --export-wires <project.qet> <output.csv>
qelectrotech --export-bom <project.qet> <output.csv> [--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.
+156 -2
View File
@@ -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<void()> &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<Attribute> &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
+30 -1
View File
@@ -22,6 +22,8 @@
#include <QtCore>
#include <QtWidgets>
#include <functional>
/**
@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<void()> &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<Attribute> &attributes = {});
static QString blockName(const QString &name);
// you can add more functions to create more drawings.
static void drawCircle(
const QString&,
+186 -109
View File
@@ -42,6 +42,125 @@
#include <cmath>
#include <utility>
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<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 (draw_terminals) {
Createdxf::layer = Layer::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));
}
}
}
/// 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<Element *> list_elements;
QList<Conductor *> 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<QString, QString> block_names; // location -> block name
QList<Element *> block_models; // one placed symbol per block
if (properties.dxf_blocks) {
QSet<QString> 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<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) {
Createdxf::layer = Layer::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));
}
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));
}
}
+5
View File
@@ -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(
+1
View File
@@ -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
+10
View File
@@ -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
}
+1
View File
@@ -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;
+11
View File
@@ -473,6 +473,17 @@ target_link_libraries(tst_dxfcurves PRIVATE Qt::Test)
target_compile_definitions(tst_dxfcurves PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# --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=\"$<TARGET_FILE:qelectrotech>\"")
# --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.
+62
View File
@@ -0,0 +1,62 @@
<project title="DXF symbol blocks" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7b10}" version="0.200.1">
<properties>
<property name="saveddate" show="1">10/2/26</property>
<property name="saveddate-eu" show="1">02-10-2026</property>
<property name="saveddate-us" show="1">2026-10-02</property>
<property name="savedfilename" show="1">dxf_blocks</property>
<property name="savedtime" show="1">18:39</property>
</properties>
<usage enabled="true" time_spent="0"/>
<newdiagrams>
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<report label="%f-%l%c"/>
<xrefs>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
</xrefs>
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
<folio_autonums/>
<element_autonums current_autonum="" freeze_new_elements="false"/>
<guides/>
</newdiagrams>
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7a01}" version="0.200.1-dev">
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<elements>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="" type="embed://import/curves.elmt" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7a02}" x="300" y="300" z="10">
<terminals/>
<inputs/>
<dynamic_texts/>
<texts_groups/>
</element>
<element freezeLabel="false" is_movable="1" orientation="1" prefix="" type="embed://import/curves.elmt" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7a03}" x="500" y="300" z="10">
<terminals/>
<inputs/>
<dynamic_texts/>
<texts_groups/>
</element>
</elements>
</diagram>
<collection>
<category name="import">
<names>
<name lang="en">Imported elements</name>
</names>
<element name="curves.elmt">
<definition height="80" hotspot_x="10" hotspot_y="10" link_type="simple" type="element" version="0.100.0" width="120">
<uuid uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7a05}"/>
<names>
<name lang="en">Circle and arc</name>
</names>
<description>
<ellipse antialias="true" style="line-style:normal;line-weight:normal;filling:none;color:black" x="0" y="0" width="20" height="20"/>
<arc antialias="true" style="line-style:normal;line-weight:normal;filling:none;color:black" x="40" y="0" width="20" height="20" start="30" angle="120"/>
</description>
</definition>
</element>
</category>
</collection>
</project>
+96
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include <QtTest>
#include <QProcess>
#include <QTemporaryDir>
/**
--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<QPair<QString, QString>> 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<QPair<QString, QString>> 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<QPair<QString, QString>> &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"