Files
qelectrotech-source-mirror/sources/dxfpaintdevice.h
T
ispyisail d988054aca Export the master-side cross-reference table to DXF
Follow-up to #740, which fixed the slave-side "(n-Xn)" cross-reference
label. The master-side item - the small table/cross drawn next to a
report or master element, listing where each of its slaves is used -
was still missing from DXF export. Measured against examples/
industrial.qet with the PDF export as an oracle (renders the whole
scene, so it shows what should be there):

                          before  after   PDF
  slave xrefs  "(n-Xn)"       41     41    41   (already fixed, #740)
  folio/position strings     358    403   403

DXF now matches the PDF exactly.

## Why this needed a different approach than #740

The slave label is a plain QGraphicsTextItem - one string, trivial to
walk and re-emit as a single DXF TEXT entity, which is what #740 did.

The master-side item (CrossRefItem) is not: it paints itself with
~600 lines of hand-written QPainter calls across three modes
(drawAsCross/drawAsContacts/drawAsPlcTable), including a header
table, contact symbols, and rules. Hand-porting that logic to emit
DXF primitives directly would mean maintaining two divergent
implementations of the same drawing that have to be kept in sync by
hand forever.

## Approach: a QPaintEngine that intercepts CrossRefItem's own paint()

DxfPaintEngine/DxfPaintDevice (sources/dxfpaintdevice.{h,cpp}) is a
QPaintEngine/QPaintDevice pair - the same mechanism QPrinter and
QSvgGenerator use to redirect QPainter output elsewhere. Constructing
a QPainter on a DxfPaintDevice and calling item->paint() on it produces
DXF entities instead of pixels, using the exact same drawing code that
already renders correctly on screen. CrossRefItem::paint() is
unmodified.

Scope is deliberately narrow - only the QPainter calls CrossRefItem's
paint() is observed to make: drawLines -> LINE, drawRects/drawPath's
fill case -> outline-only LWPOLYLINE (no HATCH support in v1 - DXF's
fill primitive is a separate, more involved entity type; documented as
a known limitation rather than attempted here), drawEllipse -> CIRCLE
or a flattened polygon for rotated ellipses, drawPath's arc case (from
drawArc/drawPie) -> chord-flattened LINE segments, drawPolygon ->
LWPOLYLINE, drawTextItem -> TEXT. drawPixmap is intentionally
unimplemented (qWarning + skip) since CrossRefItem never calls it -
this is not a general-purpose DXF paint engine, and isn't meant to be
in this PR.

CrossRefItem::paint() is protected, per the normal QGraphicsItem
contract - added a small paintForExport() wrapper rather than making
paint() itself public, or reaching around access control.

## Explicitly out of scope

QetShapeItem::toDXF() and QetGraphicsTableItem::toDXF() (both already
implemented and working) are untouched. Rewriting working exporters
onto this engine to prove an architectural point would be a large,
unrelated diff with no user-visible benefit - if that consolidation is
wanted later, it's a separate proposal once this engine has shipped
and proven out on the one item that currently has no DXF export at
all.

## Testing

Built clean on Qt5/Linux. Verified via the GUI export dialog
(Fichier > Exporter > DXF) against examples/industrial.qet, 50 folios:
export completes without error or crash, all 50 .dxf files are
structurally well-formed (balanced SECTION/ENDSEC, single EOF each),
and grepping the folio-position pattern gives the before/after/PDF
numbers above. Spot-checked several real label strings (e.g. "18-B18",
"20-A2") present as TEXT entity values in the output, not just an
artifact of the count matching.
2026-08-15 08:39:55 +12:00

118 lines
4.4 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/>.
*/
#ifndef DXFPAINTDEVICE_H
#define DXFPAINTDEVICE_H
#include <QPaintDevice>
#include <QPaintEngine>
#include <QPen>
#include <QBrush>
#include <QFont>
#include <QString>
/**
@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