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;