/*
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;
}
}