Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
94ec4b8feThis file is pinned to the selected release. Download the original to open it in your design tool.
/*
* LibrePCB - Professional EDA for everyone!
* Copyright (C) 2013 LibrePCB Developers, see AUTHORS.md for contributors.
* https://librepcb.org/
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*******************************************************************************
* Includes
******************************************************************************/
#include "boardpainter.h"
#include "../../application.h"
#include "../../export/graphicsexportsettings.h"
#include "../../export/graphicspainter.h"
#include "../../geometry/text.h"
#include "../../library/pkg/footprint.h"
#include "../../workspace/colorrole.h"
#include "../project.h"
#include "board.h"
#include "items/bi_device.h"
#include "items/bi_hole.h"
#include "items/bi_netline.h"
#include "items/bi_netsegment.h"
#include "items/bi_pad.h"
#include "items/bi_plane.h"
#include "items/bi_polygon.h"
#include "items/bi_stroketext.h"
#include "items/bi_via.h"
#include <QtCore>
#include <QtGui>
/*******************************************************************************
* Namespace
******************************************************************************/
namespace librepcb {
/*******************************************************************************
* Constructors / Destructor
******************************************************************************/
BoardPainter::BoardPainter(const Board& board)
: mMonospaceFont(Application::getDefaultMonospaceFont()),
mCopperLayers(board.getCopperLayers()) {
// Helper to add a pad.
auto addPad = [this](const BI_Pad* pad) {
Pad padObj;
padObj.transform = Transform(*pad);
for (const PadHole& hole : pad->getProperties().getHoles()) {
padObj.holes.append(hole);
}
for (auto it = pad->getGeometries().begin();
it != pad->getGeometries().end(); it++) {
foreach (const PadGeometry& geometry, it.value()) {
padObj.layerGeometries.append(std::make_pair(it.key(), geometry));
}
}
mPads.append(padObj);
};
foreach (const BI_Device* device, board.getDeviceInstances()) {
Footprint fpt;
fpt.transform = Transform(*device);
foreach (const BI_Pad* pad, device->getPads()) {
addPad(pad);
}
for (const Polygon& polygon : device->getLibFootprint().getPolygons()) {
fpt.polygons.append(PolygonData{
&polygon.getLayer(), polygon.getPath(), polygon.getLineWidth(),
polygon.isFilled(), polygon.isGrabArea()});
}
for (const Circle& circle : device->getLibFootprint().getCircles()) {
fpt.circles.append(circle);
}
for (const Hole& hole : device->getLibFootprint().getHoles()) {
// Memorize stop mask offset now to avoid needing design rules later.
fpt.holes.append(
HoleData{hole.getDiameter(), hole.getPath(),
device->getHoleStopMasks().value(hole.getUuid())});
}
foreach (const BI_StrokeText* text, device->getStrokeTexts()) {
mStrokeTexts.append(StrokeTextData{
Transform(text->getData()), &text->getData().getLayer(),
text->getPaths(), text->getData().getHeight(),
text->getData().getStrokeWidth(), text->getSubstitutedText(),
text->getData().getAlign()});
}
mFootprints.append(fpt);
}
foreach (const BI_Plane* plane, board.getPlanes()) {
mPlanes.append(Plane{&plane->getLayer(), plane->getFragments()});
}
foreach (const BI_Polygon* polygon, board.getPolygons()) {
mPolygons.append(PolygonData{
&polygon->getData().getLayer(), polygon->getData().getPath(),
polygon->getData().getLineWidth(), polygon->getData().isFilled(),
polygon->getData().isGrabArea()});
}
foreach (const BI_StrokeText* text, board.getStrokeTexts()) {
mStrokeTexts.append(
StrokeTextData{Transform(text->getData()), &text->getData().getLayer(),
text->getPaths(), text->getData().getHeight(),
text->getData().getStrokeWidth(),
text->getSubstitutedText(), text->getData().getAlign()});
}
foreach (const BI_Hole* hole, board.getHoles()) {
mHoles.append(HoleData{hole->getData().getDiameter(),
hole->getData().getPath(),
hole->getStopMaskOffset()});
}
foreach (const BI_NetSegment* segment, board.getNetSegments()) {
foreach (const BI_Pad* pad, segment->getPads()) {
addPad(pad);
}
for (const BI_Via* via : segment->getVias()) {
mVias.append(ViaData{via->getPosition(), via->getActualDrillDiameter(),
via->getActualSize(), &via->getVia().getStartLayer(),
&via->getVia().getEndLayer(),
via->getStopMaskDiameterTop(),
via->getStopMaskDiameterBottom()});
}
for (const BI_NetLine* netline : segment->getNetLines()) {
mTraces.append(Trace{&netline->getLayer(), netline->getP1().getPosition(),
netline->getP2().getPosition(),
netline->getWidth()});
}
}
}
BoardPainter::~BoardPainter() noexcept {
}
/*******************************************************************************
* General Methods
******************************************************************************/
void BoardPainter::paint(
QPainter& painter, const GraphicsExportSettings& settings) const noexcept {
// Determine what to paint on which color layer.
initContentByColor();
// Draw THT pads & vias depending on copper layers visibility.
QSet<QString> enabledCopperLayers;
for (const QString& colorRole : settings.getPaintOrder()) {
if (ColorRole::isCopperId(colorRole)) {
enabledCopperLayers.insert(colorRole);
}
}
const bool thtOnlyOnCopperLayers = !enabledCopperLayers.isEmpty();
// Draw pad circles only if holes are enabled, but pads not.
const bool drawPadHoles =
settings.getPaintOrder().contains(ColorRole::boardHoles().getId()) &&
(!settings.getPaintOrder().contains(ColorRole::boardPads().getId()));
// Draw each color in reverse order for correct stackup.
GraphicsPainter p(painter);
p.setMinLineWidth(settings.getMinLineWidth());
foreach (const QString& color, settings.getPaintOrder()) {
if (thtOnlyOnCopperLayers &&
((color == ColorRole::boardPads().getId()) ||
(color == ColorRole::boardVias().getId()))) {
continue;
}
ColorContent content = mContentByColor.value(color);
// Draw areas.
foreach (const QPainterPath& area, content.areas) {
p.drawPath(area, Length(0), QColor(), settings.getColor(color));
}
// Draw THT pad areas.
foreach (const QPainterPath& area, content.thtPadAreas) {
p.drawPath(area, Length(0), QColor(),
settings.getColor(ColorRole::boardPads().getId()));
}
// Draw via areas.
foreach (const QPainterPath& area, content.viaAreas) {
p.drawPath(area, Length(0), QColor(),
settings.getColor(ColorRole::boardVias().getId()));
}
// Draw traces.
foreach (const Trace& trace, content.traces) {
p.drawLine(trace.startPosition, trace.endPosition, *trace.width,
settings.getColor(color));
}
// Draw polygons.
foreach (const PolygonData& polygon, content.polygons) {
p.drawPolygon(
polygon.path, *polygon.lineWidth, settings.getColor(color),
settings.getFillColor(color, polygon.filled, polygon.grabArea));
}
// Draw circles.
foreach (const Circle& circle, content.circles) {
p.drawCircle(
circle.getCenter(), *circle.getDiameter(), *circle.getLineWidth(),
settings.getColor(color),
settings.getFillColor(color, circle.isFilled(), circle.isGrabArea()));
}
// Draw holes.
foreach (const HoleData& hole, content.holes) {
p.drawSlot(*hole.path, hole.diameter, Length(0), settings.getColor(color),
QColor());
}
// Draw pad holes.
if (drawPadHoles) {
foreach (const HoleData& hole, content.padHoles) {
p.drawSlot(*hole.path, hole.diameter, Length(0),
settings.getColor(color), QColor());
}
}
// Draw invisible texts to make them selectable and searchable in PDF and
// SVG output.
foreach (const TextData& text, content.texts) {
p.drawText(text.position, text.rotation, *text.height, text.align,
text.text, mMonospaceFont, Qt::transparent, true,
settings.getMirror(), false);
}
}
}
/*******************************************************************************
* Private Methods
******************************************************************************/
void BoardPainter::initContentByColor() const noexcept {
QMutexLocker lock(&mMutex);
if (mContentByColor.isEmpty()) {
// Footprints.
foreach (const Footprint& footprint, mFootprints) {
// Footprint polygons.
foreach (PolygonData polygon, footprint.polygons) {
polygon.layer = &footprint.transform.map(*polygon.layer);
polygon.path = footprint.transform.map(polygon.path);
const QString role = polygon.layer->getColorRole().getId();
mContentByColor[role].polygons.append(polygon);
}
// Footprint circles.
foreach (Circle circle, footprint.circles) {
circle.setLayer(footprint.transform.map(circle.getLayer()));
circle.setCenter(footprint.transform.map(circle.getCenter()));
const QString role = circle.getLayer().getColorRole().getId();
mContentByColor[role].circles.append(circle);
}
// Footprint holes.
foreach (HoleData hole, footprint.holes) {
hole.path = NonEmptyPath(footprint.transform.map(hole.path));
mContentByColor[ColorRole::boardHoles().getId()].holes.append(hole);
PadGeometry geometry =
PadGeometry::stroke(hole.diameter, hole.path, {});
if (const std::optional<Length>& offset = hole.stopMaskOffset) {
geometry = geometry.withOffset(*offset);
}
const QPainterPath stopMask = geometry.toFilledQPainterPathPx();
mContentByColor[ColorRole::boardStopMaskTop().getId()].areas.append(
stopMask);
mContentByColor[ColorRole::boardStopMaskBot().getId()].areas.append(
stopMask);
}
}
// Planes.
foreach (const Plane& plane, mPlanes) {
const QString role = plane.layer->getColorRole().getId();
foreach (const Path& path, plane.fragments) {
mContentByColor[role].areas.append(path.toQPainterPathPx());
}
}
// Pads.
foreach (const Pad& pad, mPads) {
foreach (const auto& layerGeometry, pad.layerGeometries) {
const QPainterPath path =
pad.transform.mapPx(layerGeometry.second.toQPainterPathPx());
const QString role = layerGeometry.first->getColorRole().getId();
if ((!pad.holes.isEmpty()) && layerGeometry.first->isCopper()) {
ColorContent& tht = mContentByColor[ColorRole::boardPads().getId()];
if (!tht.areas.contains(path)) {
tht.areas.append(path);
}
mContentByColor[role].thtPadAreas.append(path);
} else {
mContentByColor[role].areas.append(path);
}
}
// Also add the holes for THT pads.
for (const PadHole& hole : pad.holes) {
mContentByColor[ColorRole::boardHoles().getId()].padHoles.append(
HoleData{hole.getDiameter(), pad.transform.map(hole.getPath()),
std::nullopt});
}
}
// Vias.
foreach (const ViaData& via, mVias) {
foreach (const Layer* layer, mCopperLayers) {
if ((layer->getCopperNumber() >= via.startLayer->getCopperNumber()) &&
(layer->getCopperNumber() <= via.endLayer->getCopperNumber())) {
const QPainterPath path = Via::toQPainterPathPx(via.drill, via.size)
.translated(via.position.toPxQPointF());
ColorContent& vias = mContentByColor[ColorRole::boardVias().getId()];
if (!vias.areas.contains(path)) {
vias.areas.append(path);
}
mContentByColor[layer->getColorRole().getId()].viaAreas.append(path);
}
}
auto stopMasks = {
std::make_pair(ColorRole::boardStopMaskTop().getId(),
via.stopMaskDiameterTop),
std::make_pair(ColorRole::boardStopMaskBot().getId(),
via.stopMaskDiameterBottom),
};
for (const auto& cfg : stopMasks) {
if (const auto diameter = cfg.second) {
const Path outline = Path::circle(*diameter).translated(via.position);
mContentByColor[cfg.first].areas.append(outline.toQPainterPathPx());
}
}
}
// Traces.
foreach (const Trace& trace, mTraces) {
const QString role = trace.layer->getColorRole().getId();
mContentByColor[role].traces.append(trace);
}
// Polygons.
foreach (const PolygonData& polygon, mPolygons) {
const QString role = polygon.layer->getColorRole().getId();
mContentByColor[role].polygons.append(polygon);
}
// Holes.
foreach (const HoleData& hole, mHoles) {
mContentByColor[ColorRole::boardHoles().getId()].holes.append(hole);
PadGeometry geometry = PadGeometry::stroke(hole.diameter, hole.path, {});
if (const std::optional<Length>& offset = hole.stopMaskOffset) {
geometry = geometry.withOffset(*offset);
}
const QPainterPath stopMask = geometry.toFilledQPainterPathPx();
mContentByColor[ColorRole::boardStopMaskTop().getId()].areas.append(
stopMask);
mContentByColor[ColorRole::boardStopMaskBot().getId()].areas.append(
stopMask);
}
// Texts.
foreach (const StrokeTextData& text, mStrokeTexts) {
const QString role = text.layer->getColorRole().getId();
foreach (const Path& path, text.transform.map(text.paths)) {
mContentByColor[role].polygons.append(
PolygonData{text.layer, path, text.strokeWidth, false, false});
}
Angle rotation = text.transform.mapNonMirrorable(Angle::deg0());
Alignment align =
text.transform.getMirrored() ? text.align.mirroredV() : text.align;
Length totalHeight = *text.height + *text.strokeWidth;
totalHeight += totalHeight / 2; // Correction factor from TTF to stroke.
Length baseline = totalHeight / 4; // Baseline correction TTF -> stroke.
Point baselineOffset;
if (align.getV() == VAlign::bottom()) {
baselineOffset.setY(-baseline);
} else if (align.getV() == VAlign::top()) {
baselineOffset.setY(baseline);
}
baselineOffset.rotate(rotation);
mContentByColor[role].texts.append(
TextData{text.transform.getPosition() + baselineOffset, rotation,
PositiveLength(totalHeight), align, text.text});
}
}
}
/*******************************************************************************
* End of File
******************************************************************************/
} // namespace librepcb