diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake
index 112e6dea5..603345171 100644
--- a/cmake/qet_compilation_vars.cmake
+++ b/cmake/qet_compilation_vars.cmake
@@ -177,6 +177,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/configdialog.h
${QET_DIR}/sources/createdxf.cpp
${QET_DIR}/sources/createdxf.h
+ ${QET_DIR}/sources/dxfpaintdevice.cpp
+ ${QET_DIR}/sources/dxfpaintdevice.h
${QET_DIR}/sources/diagramcommands.cpp
${QET_DIR}/sources/diagramcommands.h
${QET_DIR}/sources/diagramcontent.cpp
diff --git a/sources/dxfpaintdevice.cpp b/sources/dxfpaintdevice.cpp
new file mode 100644
index 000000000..e7f180a33
--- /dev/null
+++ b/sources/dxfpaintdevice.cpp
@@ -0,0 +1,269 @@
+/*
+ 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 "dxfpaintdevice.h"
+#include "createdxf.h"
+
+#include
+#include
+#include
+#include
+
+/**
+ @brief DxfPaintEngine::DxfPaintEngine
+ @param filepath path of the already-open DXF file entities are
+ appended to (see Createdxf::dxfBegin()/dxfEnd()).
+*/
+DxfPaintEngine::DxfPaintEngine(const QString &filepath) :
+ QPaintEngine(QPaintEngine::AllFeatures),
+ m_filepath(filepath)
+{
+}
+
+bool DxfPaintEngine::begin(QPaintDevice *pdev)
+{
+ Q_UNUSED(pdev)
+ setActive(true);
+ return true;
+}
+
+bool DxfPaintEngine::end()
+{
+ setActive(false);
+ return true;
+}
+
+/**
+ @brief DxfPaintEngine::updateState
+ Track the properties this engine cares about. Everything drawn is
+ mapped through the transform in effect at draw time - this is what
+ lets an item's paint() be replayed unmodified at whatever scene
+ position/rotation it actually has, exactly as QPainter does for
+ on-screen painting.
+*/
+void DxfPaintEngine::updateState(const QPaintEngineState &state)
+{
+ QPaintEngine::DirtyFlags flags = state.state();
+
+ if (flags & QPaintEngine::DirtyTransform)
+ m_world_transform = state.transform();
+ if (flags & QPaintEngine::DirtyPen)
+ m_pen = state.pen();
+ if (flags & QPaintEngine::DirtyBrush)
+ m_brush = state.brush();
+ if (flags & QPaintEngine::DirtyFont)
+ m_font = state.font();
+}
+
+QPointF DxfPaintEngine::toDxf(const QPointF &local_point) const
+{
+ // Createdxf's QRectF/QLineF/QPolygonF/QPointF convenience overloads
+ // each apply xScale/yScale and the sheetHeight Y-flip internally, so
+ // this engine's whole job is composing the world transform - not
+ // touching DXF units at all.
+ return m_world_transform.map(local_point);
+}
+
+void DxfPaintEngine::strokePolygon(const QPolygonF &poly, bool close)
+{
+ if (poly.size() < 2)
+ return;
+ if (m_pen.style() == Qt::NoPen)
+ return;
+
+ QPolygonF mapped;
+ mapped.reserve(poly.size());
+ for (const QPointF &p : poly)
+ mapped << toDxf(p);
+
+ if (close && mapped.first() != mapped.last())
+ mapped << mapped.first();
+
+ Createdxf::drawPolyline(m_filepath, mapped, Createdxf::dxfColor(m_pen));
+}
+
+void DxfPaintEngine::drawLines(const QLineF *lines, int lineCount)
+{
+ if (m_pen.style() == Qt::NoPen)
+ return;
+ for (int i = 0; i < lineCount; ++i)
+ {
+ QLineF mapped(toDxf(lines[i].p1()), toDxf(lines[i].p2()));
+ Createdxf::drawLine(m_filepath, mapped, Createdxf::dxfColor(m_pen));
+ }
+}
+
+void DxfPaintEngine::drawRects(const QRectF *rects, int rectCount)
+{
+ for (int i = 0; i < rectCount; ++i)
+ {
+ QPolygonF corners;
+ corners << rects[i].topLeft() << rects[i].topRight()
+ << rects[i].bottomRight() << rects[i].bottomLeft();
+
+ // No HATCH support in v1 (see the design note in the header) - a
+ // filled rect is emitted as its outline only, same as an unfilled
+ // one. Still emitted (not skipped) when there's no pen but there
+ // is a brush, so a filled-only shape isn't dropped entirely.
+ if (m_pen.style() != Qt::NoPen || m_brush.style() != Qt::NoBrush)
+ strokePolygon(corners, true);
+ }
+}
+
+void DxfPaintEngine::drawEllipse(const QRectF &rect)
+{
+ if (m_pen.style() == Qt::NoPen && m_brush.style() == Qt::NoBrush)
+ return;
+
+ // A non axis-aligned ellipse (rotation baked into m_world_transform)
+ // can't be expressed as Createdxf::drawEllipse(), which only accepts
+ // an axis-aligned QRectF. Approximate with a flattened polygon in
+ // that case; use the exact CIRCLE/ELLIPSE entity when the transform
+ // is axis-aligned, matching what the existing hand-written exporters
+ // already produce for unrotated shapes.
+ const bool axis_aligned = qFuzzyIsNull(m_world_transform.m12())
+ && qFuzzyIsNull(m_world_transform.m21());
+ if (axis_aligned)
+ {
+ QRectF mapped = m_world_transform.mapRect(rect);
+ Createdxf::drawEllipse(m_filepath, mapped, Createdxf::dxfColor(m_pen));
+ return;
+ }
+
+ QPainterPath path;
+ path.addEllipse(rect);
+ const auto polygons = path.toSubpathPolygons();
+ for (const QPolygonF &poly : polygons)
+ strokePolygon(poly, true);
+}
+
+/**
+ @brief DxfPaintEngine::drawPath
+ Handles drawArc()/drawPie() (QPainterPath::Arc elements - emitted as
+ chord segments, since Createdxf's ARC-equivalent
+ (drawArcEllipse()) only takes an axis-aligned ellipse + angle pair,
+ which doesn't compose with an arbitrary path transform) and
+ fillPath()/generic paths (flattened to polylines).
+*/
+void DxfPaintEngine::drawPath(const QPainterPath &path)
+{
+ if (m_pen.style() == Qt::NoPen && m_brush.style() == Qt::NoBrush)
+ return;
+
+ const auto polygons = path.toSubpathPolygons();
+ for (const QPolygonF &poly : polygons)
+ strokePolygon(poly, path.isEmpty() ? false : true);
+}
+
+void DxfPaintEngine::drawPolygon(const QPointF *points, int pointCount, PolygonDrawMode mode)
+{
+ if (pointCount < 2)
+ return;
+ QPolygonF poly;
+ poly.reserve(pointCount);
+ for (int i = 0; i < pointCount; ++i)
+ poly << points[i];
+
+ strokePolygon(poly, mode != PolylineMode);
+}
+
+void DxfPaintEngine::drawTextItem(const QPointF &p, const QTextItem &textItem)
+{
+ qreal font_size = textItem.font().pointSizeF();
+ if (font_size < 0)
+ font_size = textItem.font().pixelSize();
+
+ // Rotation: extract the angle baked into the world transform by
+ // mapping a unit vector, the same approach used to compute rotation
+ // for the existing element-text DXF export in exportdialog.cpp.
+ QPointF origin = m_world_transform.map(p);
+ QPointF right = m_world_transform.map(p + QPointF(1, 0));
+ qreal angle = qRadiansToDegrees(qAtan2(-(right.y() - origin.y()), right.x() - origin.x()));
+
+ Createdxf::drawText(m_filepath, textItem.text(), toDxf(p), font_size,
+ angle, Createdxf::dxfColor(m_pen), 0.72);
+}
+
+/**
+ @brief DxfPaintEngine::drawPixmap
+ Not implemented in v1 - out of scope per the design note in the
+ header (CrossRefItem, the only item exported through this engine so
+ far, never draws a pixmap). qWarning() rather than a hard failure, so
+ an item that does call this in the future degrades to "one entity
+ missing" instead of crashing the whole export.
+*/
+void DxfPaintEngine::drawPixmap(const QRectF &r, const QPixmap &pm, const QRectF &sr)
+{
+ Q_UNUSED(r)
+ Q_UNUSED(pm)
+ Q_UNUSED(sr)
+ qWarning() << "DxfPaintEngine::drawPixmap: not supported, entity skipped";
+}
+
+/**
+ @brief DxfPaintDevice::DxfPaintDevice
+ @param filepath path of the already-open DXF file (see
+ Createdxf::dxfBegin()/dxfEnd() - this device only appends entities in
+ between, it does not open/close the file itself).
+*/
+DxfPaintDevice::DxfPaintDevice(const QString &filepath) :
+ QPaintDevice(),
+ m_filepath(filepath),
+ m_engine(new DxfPaintEngine(filepath))
+{
+}
+
+DxfPaintDevice::~DxfPaintDevice()
+{
+ delete m_engine;
+}
+
+QPaintEngine *DxfPaintDevice::paintEngine() const
+{
+ return m_engine;
+}
+
+int DxfPaintDevice::metric(PaintDeviceMetric metric) const
+{
+ // A DXF document has no pixel grid or physical size in the sense
+ // QPaintDevice::metric() expects - large-but-finite values here are
+ // enough to keep QPainter's own bookkeeping (e.g. clip region setup)
+ // from misbehaving; nothing in this engine actually consults them.
+ switch (metric)
+ {
+ case PdmWidth:
+ case PdmHeight:
+ return 1000000;
+ case PdmDpiX:
+ case PdmDpiY:
+ case PdmPhysicalDpiX:
+ case PdmPhysicalDpiY:
+ return 96;
+ case PdmWidthMM:
+ case PdmHeightMM:
+ return 1000000;
+ case PdmNumColors:
+ return 16777216;
+ case PdmDepth:
+ return 24;
+ case PdmDevicePixelRatio:
+ case PdmDevicePixelRatioScaled:
+ return 1;
+ default:
+ return 0;
+ }
+}
diff --git a/sources/dxfpaintdevice.h b/sources/dxfpaintdevice.h
new file mode 100644
index 000000000..6b43472c5
--- /dev/null
+++ b/sources/dxfpaintdevice.h
@@ -0,0 +1,117 @@
+/*
+ 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 .
+*/
+#ifndef DXFPAINTDEVICE_H
+#define DXFPAINTDEVICE_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+/**
+ @brief The DxfPaintEngine class
+ A QPaintEngine that translates the small set of QPainter calls made by
+ QGraphicsItem::paint() implementations (drawLines, drawRects,
+ drawEllipse, drawPolygon/drawPolyline, drawTextItem) into DXF entities
+ written through Createdxf, instead of pixels.
+
+ This exists so that an item's existing, already-correct paint() code
+ can be reused unmodified to produce a DXF export: construct a QPainter
+ on a DxfPaintDevice targeting the item, call painter.begin()/end()
+ around item->paint(&painter, ...), and every primitive the item draws
+ becomes a DXF entity at the item's scene position instead of a pixel
+ on screen.
+
+ Scope (deliberately not the full QPainter surface - only what
+ QElectroTech's own paint() implementations are observed to call):
+ - drawLines -> LINE
+ - drawRects -> LWPOLYLINE (closed 4-point outline; DXF has no
+ filled-rect primitive in the AC1006 dialect this
+ exporter targets, so brush fill is not emitted -
+ see fillPath below)
+ - drawEllipse -> Createdxf::drawArcEllipse (full sweep) or CIRCLE
+ - drawPath -> arcs (from QPainterPath::Arc elements, used by
+ drawArc/drawPie) become a sequence of LINE chords;
+ anything else in the path is flattened to
+ polyline segments via QPainterPath::toSubpathPolygons
+ - drawPolygon -> LWPOLYLINE
+ - drawTextItem -> TEXT (drawText() calls route through this)
+ - fillPath -> same outline-only handling as drawRects; no HATCH
+ support in v1 (see design note in the PR)
+
+ Anything outside this list (images, gradients, etc.) is intentionally
+ unimplemented and asserts in debug builds rather than silently
+ producing an incomplete drawing - callers should know immediately if
+ an item they're exporting uses something this engine doesn't cover
+ yet, rather than getting a DXF file quietly missing content.
+*/
+class DxfPaintEngine : public QPaintEngine
+{
+ public:
+ explicit DxfPaintEngine(const QString &filepath);
+
+ bool begin(QPaintDevice *pdev) override;
+ bool end() override;
+ void updateState(const QPaintEngineState &state) override;
+
+ void drawLines(const QLineF *lines, int lineCount) override;
+ void drawRects(const QRectF *rects, int rectCount) override;
+ void drawEllipse(const QRectF &rect) override;
+ void drawPath(const QPainterPath &path) override;
+ void drawPolygon(const QPointF *points, int pointCount, PolygonDrawMode mode) override;
+ void drawTextItem(const QPointF &p, const QTextItem &textItem) override;
+ void drawPixmap(const QRectF &r, const QPixmap &pm, const QRectF &sr) override;
+
+ Type type() const override { return QPaintEngine::User; }
+
+ private:
+ QPointF toDxf(const QPointF &scene_point) const;
+ void strokePolygon(const QPolygonF &poly, bool close);
+
+ QString m_filepath;
+ QTransform m_world_transform;
+ QPen m_pen;
+ QBrush m_brush;
+ QFont m_font;
+};
+
+/**
+ @brief The DxfPaintDevice class
+ Pairs with DxfPaintEngine. One instance targets one already-open DXF
+ file (Createdxf::dxfBegin()/dxfEnd() bracket the whole export, same as
+ today - this device only ever appends entities in between).
+*/
+class DxfPaintDevice : public QPaintDevice
+{
+ public:
+ explicit DxfPaintDevice(const QString &filepath);
+ ~DxfPaintDevice() override;
+
+ QPaintEngine *paintEngine() const override;
+
+ protected:
+ int metric(PaintDeviceMetric metric) const override;
+
+ private:
+ QString m_filepath;
+ DxfPaintEngine *m_engine;
+};
+
+#endif // DXFPAINTDEVICE_H
diff --git a/sources/exportdialog.cpp b/sources/exportdialog.cpp
index 278bf7b36..e0315048c 100644
--- a/sources/exportdialog.cpp
+++ b/sources/exportdialog.cpp
@@ -22,8 +22,10 @@
#include "exportpropertieswidget.h"
#include "factory/elementpicturefactory.h"
#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
+#include "dxfpaintdevice.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"
@@ -467,6 +469,12 @@ void ExportDialog::generateDxf(
//as plain QGraphicsTextItem children, so neither cast below picks them
//up and they were missing from the DXF entirely.
QList 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 list_master_xrefs;
QList list_lines;
QList list_rectangles;
//QList list_ellipses;
@@ -493,6 +501,8 @@ void ExportDialog::generateDxf(
}
} else if (QetGraphicsTableItem *gti = qgraphicsitem_cast(qgi)) {
list_tables << gti;
+ } else if (CrossRefItem *xref = qgraphicsitem_cast(qgi)) {
+ list_master_xrefs << xref;
}
}
@@ -722,6 +732,18 @@ void ExportDialog::generateDxf(
}
}
+ //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();
+ }
+
Createdxf::dxfEnd(file_path);
saveReloadDiagramParameters(diagram, false);
diff --git a/sources/qetgraphicsitem/crossrefitem.h b/sources/qetgraphicsitem/crossrefitem.h
index 6c11374fd..ec3cf7070 100644
--- a/sources/qetgraphicsitem/crossrefitem.h
+++ b/sources/qetgraphicsitem/crossrefitem.h
@@ -22,6 +22,7 @@
#include
#include
+#include
class Element;
class DynamicElementTextItem;
@@ -89,6 +90,19 @@ class CrossRefItem : public QGraphicsObject
void updateLabel();
void autoPos();
+ public:
+ /// DXF export: replay this item's paint() on an arbitrary QPainter
+ /// (e.g. one targeting DxfPaintDevice). paint() itself stays
+ /// protected, as it should for the normal
+ /// QGraphicsScene/QGraphicsView paint contract - this is a
+ /// deliberate, narrow escape hatch for exporters, not a general
+ /// relaxation of that contract.
+ void paintForExport(QPainter *painter)
+ {
+ QStyleOptionGraphicsItem option;
+ paint(painter, &option, nullptr);
+ }
+
protected:
bool sceneEvent(QEvent *event) override;
void paint(QPainter *painter,
diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.h b/sources/qetgraphicsitem/dynamicelementtextitem.h
index 85826eb8f..935a98006 100644
--- a/sources/qetgraphicsitem/dynamicelementtextitem.h
+++ b/sources/qetgraphicsitem/dynamicelementtextitem.h
@@ -89,6 +89,9 @@ class DynamicElementTextItem : public DiagramTextItem
Element *parentElement() const;
/// PDF export: slave cross-reference text item ("(folio-pos)") and its master target.
QGraphicsTextItem *slaveXrefItem() const { return m_slave_Xref_item; }
+ /// DXF export: the master-side cross-reference item (the table/cross
+ /// drawn next to a report/master element), if this text item has one.
+ CrossRefItem *masterXrefItem() const { return m_Xref_item; }
Element *masterElement() const { return m_master_element.data(); }
ElementTextItemGroup *parentGroup() const;
Element *elementUseForInfo() const;