Files
qelectrotech-source-mirror/sources/dxfexport.cpp
T
ispyisail c3ed2daca8 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>
2026-10-07 09:50:18 +13:00

551 lines
19 KiB
C++

/*
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 "shownkinds.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 "textlines.h"
#include <QGraphicsSimpleTextItem>
#include <QSet>
#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
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;
//One scale for both axes, the largest that fits the sheet: two
//scales stretched the drawing and turned every circle into an oval
//(issue #1339).
const double scale = qMin(Createdxf::sheetWidth / double(width),
Createdxf::sheetHeight / double(height));
Createdxf::xScale = scale;
Createdxf::yScale = scale;
// 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)) {
//Left out like on screen and in print (View > Show, #301)
if (ShownKinds::isHidden(qgi)) {
continue;
}
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;
QGraphicsTextItem *xref = deti->slaveXrefItem();
if (xref && !ShownKinds::isHidden(xref)) {
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;
}
}
//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());
// Draw tables
Createdxf::layer = Layer::Tables;
foreach (QetGraphicsTableItem *gti, list_tables) {
gti->toDXF(file_path);
}
//Draw elements: each one in full, or as an INSERT of its block
foreach(Element *elmt, list_elements)
{
const double rotation_angle = elmt -> orientation() * 90;
const qreal elem_pos_x = elmt -> pos().x();
const qreal elem_pos_y = elmt -> pos().y();
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));
}
}
//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::layer = Layer::Wires;
//The wire's own colour, as on the folio (a two-colour wire gets
//its main one). Black comes out as BYLAYER, see Createdxf.
const ConductorProperties wire_properties = cond -> properties();
Createdxf::drawPolyline(file_path, poly, Createdxf::dxfColor(wire_properties.color));
//Draw conductor text item
Createdxf::layer = Layer::WireNumbers;
ConductorTextItem *textItem = cond -> textItem();
if (textItem && !ShownKinds::isHidden(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,
Createdxf::dxfColor(wire_properties.text_color), 0.72 );
x += offset * xdir;
y -= offset * ydir;
}
}
// Draw the junctions
Createdxf::layer = Layer::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) {
//A free text, or a symbol's own text (its label and the like)
Createdxf::layer = qgraphicsitem_cast<IndependentTextItem *>(dti)
? Layer::Texts : Layer::SymbolTexts;
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;
//As drawn: a text with a width is wrapped
QStringList lines = TextLines::layoutLines(dti -> document());
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
Createdxf::layer = Layer::Xrefs;
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().
Createdxf::layer = Layer::Xrefs;
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.
Createdxf::layer = Layer::Images;
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);
}