Loading files
9ac9868b1This 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 "boardplanefragmentsbuilder.h"
#include "../../library/pkg/footprint.h"
#include "../../library/pkg/footprintpad.h"
#include "../../utils/clipperhelpers.h"
#include "../../utils/transform.h"
#include "../circuit/netsignal.h"
#include "board.h"
#include "items/bi_device.h"
#include "items/bi_hole.h"
#include "items/bi_netline.h"
#include "items/bi_netpoint.h"
#include "items/bi_netsegment.h"
#include "items/bi_pad.h"
#include "items/bi_plane.h"
#include "items/bi_polygon.h"
#include "items/bi_via.h"
#include "items/bi_zone.h"
#include <polyclipping/clipper.hpp>
#include <QtConcurrent>
#include <QtCore>
#include <algorithm>
/*******************************************************************************
* Namespace
******************************************************************************/
namespace librepcb {
/*******************************************************************************
* Class BoardPlaneFragmentsBuilder::Result
******************************************************************************/
void BoardPlaneFragmentsBuilder::Result::throwOnError() const {
if (!errors.isEmpty()) {
throw LogicError(
__FILE__, __LINE__,
QString("Plane rebuild failed with %1 errors. First error: %2")
.arg(errors.count())
.arg(errors.first()));
}
}
bool BoardPlaneFragmentsBuilder::Result::applyToBoard() noexcept {
bool modified = false;
if (board) {
for (auto it = planes.begin(); it != planes.end(); it++) {
if (BI_Plane* plane = board->getPlanes().value(it.key())) {
modified = modified || (plane->getFragments() != it.value());
plane->setCalculatedFragments(it.value());
}
}
if (!finished) {
// Job did not finish completely, thus re-schedule all layers.
foreach (const Layer* layer, layers) {
board->invalidatePlanes(layer);
}
}
}
return modified;
}
/*******************************************************************************
* Constructors / Destructor
******************************************************************************/
BoardPlaneFragmentsBuilder::BoardPlaneFragmentsBuilder(QObject* parent) noexcept
: QObject(parent), mFuture(), mAbort(false) {
}
BoardPlaneFragmentsBuilder::~BoardPlaneFragmentsBuilder() noexcept {
cancel();
}
/*******************************************************************************
* General Methods
******************************************************************************/
QHash<Uuid, QVector<Path>> BoardPlaneFragmentsBuilder::runAndApply(
Board& board, const QSet<const Layer*>* layers) {
if (start(board, layers)) {
Result result = waitForFinished();
result.throwOnError(); // can throw
result.applyToBoard();
return result.planes;
}
return QHash<Uuid, QVector<Path>>();
}
bool BoardPlaneFragmentsBuilder::start(
Board& board, const QSet<const Layer*>* layers) noexcept {
if (auto data = createJob(board, layers)) {
cancel();
mFuture =
QtConcurrent::run(&BoardPlaneFragmentsBuilder::run, this, &board, data);
return true;
} else {
return false;
}
}
BoardPlaneFragmentsBuilder::Result BoardPlaneFragmentsBuilder::waitForFinished()
const noexcept {
return mFuture.result();
}
bool BoardPlaneFragmentsBuilder::isBusy() const noexcept {
return (mFuture.isStarted() || mFuture.isRunning()) &&
(!mFuture.isFinished()) && (!mFuture.isCanceled());
}
void BoardPlaneFragmentsBuilder::cancel() noexcept {
mAbort = true;
mFuture.waitForFinished();
mAbort = false;
}
/*******************************************************************************
* Private Methods
******************************************************************************/
std::shared_ptr<BoardPlaneFragmentsBuilder::JobData>
BoardPlaneFragmentsBuilder::createJob(
Board& board, const QSet<const Layer*>* filter) noexcept {
QSet<const Layer*> layersWithPlanes;
foreach (const BI_Plane* plane, board.getPlanes()) {
if ((!filter) ||
(plane->isVisible() && filter->contains(&plane->getLayer()))) {
layersWithPlanes.insert(&plane->getLayer());
}
}
QSet<const Layer*> layers =
board.takeScheduledLayersForPlanesRebuild(layersWithPlanes);
if (!filter) {
layers |= layersWithPlanes;
}
if (layers.isEmpty()) {
return nullptr;
}
auto data = std::make_shared<JobData>();
data->layers = Toolbox::toList(layers);
layers.insert(&Layer::boardOutlines());
layers.insert(&Layer::boardCutouts());
foreach (const BI_Device* device, board.getDeviceInstances()) {
const Transform transform(*device);
foreach (const BI_Pad* pad, device->getPads()) {
std::optional<Uuid> netSignalUuid;
if (const NetSignal* netSignal = pad->getNetSignal()) {
netSignalUuid = netSignal->getUuid();
}
data->pads.append(PadData{Transform(*pad), netSignalUuid,
pad->getProperties().getCopperClearance(),
pad->getGeometries()});
}
for (const Polygon& polygon : device->getLibFootprint().getPolygons()) {
const Layer& layer = transform.map(polygon.getLayer());
if (layers.contains(&layer)) {
data->polygons.append(
PolygonData{transform, &layer, std::nullopt, polygon.getPath(),
polygon.getLineWidth(), polygon.isFilled()});
}
}
for (const Circle& circle : device->getLibFootprint().getCircles()) {
const Layer& layer = transform.map(circle.getLayer());
if (layers.contains(&layer)) {
data->polygons.append(PolygonData{
transform, &layer, std::nullopt,
Path::circle(circle.getDiameter()).translated(circle.getCenter()),
circle.getLineWidth(), circle.isFilled()});
}
}
for (const Zone& zone : device->getLibFootprint().getZones()) {
if (zone.getRules().testFlag(Zone::Rule::NoPlanes)) {
data->keepoutZones.append(KeepoutZoneData{
transform, zone.getLayers(), {}, zone.getOutline()});
}
}
for (const Hole& hole : device->getLibFootprint().getHoles()) {
data->holes.append(
std::make_tuple(transform, hole.getDiameter(), hole.getPath()));
}
foreach (const BI_StrokeText* text, device->getStrokeTexts()) {
if (layers.contains(&text->getData().getLayer())) {
foreach (const Path& path, text->getPaths()) {
data->polygons.append(PolygonData{
Transform(text->getData()), &text->getData().getLayer(),
std::nullopt, path, text->getData().getStrokeWidth(), false});
}
}
}
}
auto toOptionalClearance = [](const UnsignedLength& clearance) {
if (clearance > 1) {
return std::make_optional(clearance);
} else if (clearance == 1) {
return std::make_optional(UnsignedLength(0));
} else {
return std::optional<UnsignedLength>();
}
};
foreach (const BI_Plane* plane, board.getPlanes()) {
if (layers.contains(&plane->getLayer())) {
data->planes.append(
PlaneData{plane->getUuid(), &plane->getLayer(),
plane->getNetSignal()
? std::make_optional(plane->getNetSignal()->getUuid())
: std::nullopt,
plane->getOutline(), plane->getMinWidth(),
plane->getMinClearanceToCopper(),
toOptionalClearance(plane->getMinClearanceToBoard()),
toOptionalClearance(plane->getMinClearanceToNpth()),
plane->getKeepIslands(), plane->getPriority(),
plane->getConnectStyle(), plane->getThermalGap(),
plane->getThermalSpokeWidth()});
}
}
foreach (const BI_Zone* zone, board.getZones()) {
if (zone->getData().getRules().testFlag(Zone::Rule::NoPlanes)) {
data->keepoutZones.append(KeepoutZoneData{Transform(), Zone::Layers(),
zone->getData().getLayers(),
zone->getData().getOutline()});
}
}
foreach (const BI_Polygon* polygon, board.getPolygons()) {
if (layers.contains(&polygon->getData().getLayer())) {
data->polygons.append(PolygonData{
Transform(), &polygon->getData().getLayer(), std::nullopt,
polygon->getData().getPath(), polygon->getData().getLineWidth(),
polygon->getData().isFilled()});
}
}
foreach (const BI_StrokeText* text, board.getStrokeTexts()) {
if (layers.contains(&text->getData().getLayer())) {
foreach (const Path& path, text->getPaths()) {
data->polygons.append(PolygonData{
Transform(text->getData()), &text->getData().getLayer(),
std::nullopt, path, text->getData().getStrokeWidth(), false});
}
}
}
foreach (const BI_Hole* hole, board.getHoles()) {
data->holes.append(std::make_tuple(
Transform(), hole->getData().getDiameter(), hole->getData().getPath()));
}
foreach (const BI_NetSegment* segment, board.getNetSegments()) {
std::optional<Uuid> netSignalUuid;
if (const NetSignal* netSignal = segment->getNetSignal()) {
netSignalUuid = netSignal->getUuid();
}
for (const BI_Pad* pad : segment->getPads()) {
data->pads.append(PadData{Transform(*pad), netSignalUuid,
pad->getProperties().getCopperClearance(),
pad->getGeometries()});
}
for (const BI_Via* via : segment->getVias()) {
data->vias.append(ViaData{
netSignalUuid, via->getVia().getPosition(), via->getActualSize(),
&via->getVia().getStartLayer(), &via->getVia().getEndLayer()});
}
for (const BI_NetLine* netline : segment->getNetLines()) {
if (layers.contains(&netline->getLayer())) {
data->traces.append(TraceData{
&netline->getLayer(), netSignalUuid, netline->getP1().getPosition(),
netline->getP2().getPosition(), netline->getWidth()});
}
}
}
return data;
}
BoardPlaneFragmentsBuilder::Result BoardPlaneFragmentsBuilder::run(
QPointer<Board> board, std::shared_ptr<JobData> data) noexcept {
// Note: This method is called from a different thread, thus be careful with
// calling other methods to only call thread-safe methods!
QElapsedTimer timer;
timer.start();
qDebug() << "Start calculating areas of" << data->planes.count()
<< "plane(s) on" << data->layers.count() << "layer(s)...";
emit started();
// Prepare result.
Result result;
result.board = board;
result.layers = Toolbox::toSet(data->layers);
try {
// Preprocess data.
for (KeepoutZoneData& zone : data->keepoutZones) {
if (zone.layers.testFlag(Zone::Layer::Top)) {
zone.boardLayers.insert(&zone.transform.map(Layer::topCopper()));
}
if (zone.layers.testFlag(Zone::Layer::Inner)) {
foreach (const Layer* layer, data->layers) {
if (layer->isInner()) {
zone.boardLayers.insert(layer);
}
}
}
if (zone.layers.testFlag(Zone::Layer::Bottom)) {
zone.boardLayers.insert(&zone.transform.map(Layer::botCopper()));
}
zone.outline = zone.transform.map(zone.outline.toClosedPath());
}
for (PolygonData& polygon : data->polygons) {
polygon.path = polygon.transform.map(polygon.path);
}
for (auto& tuple : data->holes) {
std::get<2>(tuple) = std::get<0>(tuple).map(std::get<2>(tuple));
}
for (const TraceData& trace : data->traces) {
data->polygons.append(
PolygonData{Transform(), trace.layer, trace.netSignal,
Path({Vertex(trace.startPos), Vertex(trace.endPos)}),
positiveToUnsigned(trace.width), false});
}
data->traces.clear();
// Determine board area.
QVector<Path> boardOutlines;
QVector<Path> boardCutouts;
foreach (const PolygonData& polygon, data->polygons) {
if ((polygon.layer == &Layer::boardOutlines()) &&
polygon.path.isClosed()) {
boardOutlines.append(polygon.path);
} else if ((polygon.layer == &Layer::boardCutouts()) &&
polygon.path.isClosed()) {
boardCutouts.append(polygon.path);
}
}
data->boardArea = std::make_shared<ClipperLib::Paths>(
ClipperHelpers::convert(boardOutlines, maxArcTolerance()));
ClipperHelpers::subtract(
*data->boardArea,
ClipperHelpers::convert(boardCutouts, maxArcTolerance()),
ClipperLib::pftNonZero, ClipperLib::pftNonZero);
// Sort planes: First by priority, then by uuid to get a really unique
// priority order over all existing planes. This way we can ensure that even
// planes with the same priority will always be filled in the same order.
// Random order would be dangerous!
std::sort(data->planes.begin(), data->planes.end(),
[](const PlaneData& p1, const PlaneData& p2) {
if (p1.priority != p2.priority) {
return p1.priority >= p2.priority;
} else {
return p1.uuid >= p2.uuid;
}
});
// Calculate planes for each layer in a separate thread, except the last
// one to keep this thread busy too.
QList<QFuture<LayerJobResult>> futures;
for (int i = 0; i < data->layers.count(); ++i) {
const Layer* layer = data->layers.at(i);
if (i < data->layers.count() - 1) {
// Run in other thread -> Copy JobData for safe concurrent access.
futures.append(
QtConcurrent::run(&BoardPlaneFragmentsBuilder::runLayer, this,
std::make_shared<const JobData>(*data), layer));
} else {
// Run in this thread -> no copy of JobData required.
const LayerJobResult res = runLayer(data, layer);
result.planes.insert(res.planes);
result.errors.append(res.errors);
}
}
// Fetch result of each thread (blocking until all threads finished).
foreach (const auto& future, futures) {
const LayerJobResult res = future.result();
result.planes.insert(res.planes);
result.errors.append(res.errors);
}
} catch (const Exception& e) {
qCritical() << "Failed to calculate plane fragments:" << e.getMsg();
result.errors.append(e.getMsg());
}
if (mAbort) {
result.finished = false;
qDebug() << "Aborted calculating plane areas after" << timer.elapsed()
<< "ms.";
} else {
result.finished = true;
qDebug() << "Calculated plane areas in" << timer.elapsed() << "ms.";
}
emit finished(result);
return result;
}
BoardPlaneFragmentsBuilder::LayerJobResult BoardPlaneFragmentsBuilder::runLayer(
std::shared_ptr<const JobData> data, const Layer* layer) noexcept {
LayerJobResult result;
// Build all planes.
for (auto it = data->planes.begin(); it != data->planes.end(); it++) {
if (it->layer != layer) continue;
try {
ClipperLib::Paths removedAreas;
ClipperLib::Paths connectedNetSignalAreas;
// Start with board outline shrinked by the given clearance and clipped
// to the plane outline.
// Except if the board clearance is zero, in this case we don't clip the
// plane to the board outlines.
const ClipperLib::Path planeOutline = ClipperHelpers::convert(
it->outline.toClosedPath(), maxArcTolerance());
ClipperLib::Paths fragments = *data->boardArea;
if (it->minClearanceToBoard) {
fragments = *data->boardArea;
if (*it->minClearanceToBoard != 0) {
ClipperHelpers::offset(fragments, -(*(it->minClearanceToBoard)),
maxArcTolerance()); // can throw
}
ClipperHelpers::intersect(fragments, {planeOutline},
ClipperLib::pftEvenOdd,
ClipperLib::pftEvenOdd); // can throw
} else {
fragments = {planeOutline};
}
const ClipperLib::Paths fullPlaneArea = fragments;
if (mAbort) {
break;
}
// Collect other planes.
for (auto otherIt = data->planes.begin(); otherIt != it; otherIt++) {
if ((otherIt->layer == it->layer) &&
(otherIt->netSignal != it->netSignal)) {
const UnsignedLength clearance =
std::max(it->minClearanceToCopper, otherIt->minClearanceToCopper);
ClipperLib::Paths clipperPaths = ClipperHelpers::convert(
result.planes.value(otherIt->uuid), maxArcTolerance());
ClipperHelpers::offset(clipperPaths, *clearance,
maxArcTolerance()); // can throw
removedAreas.insert(removedAreas.end(), clipperPaths.begin(),
clipperPaths.end());
}
}
if (mAbort) {
break;
}
// Collect keepout zones.
foreach (const KeepoutZoneData& zone, data->keepoutZones) {
if (zone.boardLayers.contains(it->layer)) {
const ClipperLib::Path clipperPath =
ClipperHelpers::convert(zone.outline, maxArcTolerance());
removedAreas.push_back(clipperPath);
}
}
// Collect holes, if clearance to holes is enabled
if (it->minClearanceToNpth) {
foreach (const auto& tuple, data->holes) {
const PositiveLength diameter(std::get<1>(tuple) +
*(it->minClearanceToNpth) * 2);
const QVector<Path> paths =
std::get<2>(tuple)->toOutlineStrokes(diameter);
const ClipperLib::Paths clipperPaths =
ClipperHelpers::convert(paths, maxArcTolerance());
removedAreas.insert(removedAreas.end(), clipperPaths.begin(),
clipperPaths.end());
}
}
if (mAbort) {
break;
}
// Collect vias.
foreach (const ViaData& via, data->vias) {
if ((via.startLayer->getCopperNumber() >
it->layer->getCopperNumber()) ||
(via.endLayer->getCopperNumber() < it->layer->getCopperNumber())) {
continue;
}
if (it->netSignal && (via.netSignal == it->netSignal)) {
// Via has same net as plane -> no cut-out.
// Note: Do not respect the plane connect style for vias, but always
// connect them with solid style. Since vias are not soldered, heat
// dissipation is not an issue or often even desired. See discussion
// https://github.com/LibrePCB/LibrePCB/issues/454#issuecomment-1373402172
const Path path = Path::circle(via.diameter).translated(via.position);
connectedNetSignalAreas.push_back(
ClipperHelpers::convert(path, maxArcTolerance()));
} else {
// Vias has different net than plane -> subtract with clearance.
const Path path =
Path::circle(
PositiveLength(via.diameter + it->minClearanceToCopper * 2))
.translated(via.position);
const ClipperLib::Path clipperPath =
ClipperHelpers::convert(path, maxArcTolerance());
removedAreas.push_back(clipperPath);
}
}
if (mAbort) {
break;
}
// Collect traces & other strokes.
foreach (const PolygonData& polygon, data->polygons) {
if (polygon.layer == it->layer) {
if (it->netSignal && (polygon.netSignal == it->netSignal)) {
// Same net signal -> memorize as connected area.
if (polygon.filled) {
// Area.
const ClipperLib::Path clipperPath =
ClipperHelpers::convert(polygon.path, maxArcTolerance());
connectedNetSignalAreas.push_back(clipperPath);
}
if ((!polygon.filled) || (polygon.width > 0)) {
// Outline strokes.
const QVector<Path> paths = polygon.path.toOutlineStrokes(
PositiveLength(std::max(*polygon.width, Length(1))));
const ClipperLib::Paths clipperPaths =
ClipperHelpers::convert(paths, maxArcTolerance());
connectedNetSignalAreas.insert(connectedNetSignalAreas.end(),
clipperPaths.begin(),
clipperPaths.end());
}
} else {
// Different net signal -> subtract with clearance.
if (polygon.filled) {
// Area.
ClipperLib::Paths clipperPaths{
ClipperHelpers::convert(polygon.path, maxArcTolerance())};
ClipperHelpers::offset(clipperPaths, *it->minClearanceToCopper,
maxArcTolerance()); // can throw
removedAreas.insert(removedAreas.end(), clipperPaths.begin(),
clipperPaths.end());
}
if ((!polygon.filled) || (polygon.width > 0)) {
// Outline strokes.
const QVector<Path> paths =
polygon.path.toOutlineStrokes(PositiveLength(
std::max(*polygon.width + it->minClearanceToCopper * 2,
Length(1))));
const ClipperLib::Paths clipperPaths =
ClipperHelpers::convert(paths, maxArcTolerance());
removedAreas.insert(removedAreas.end(), clipperPaths.begin(),
clipperPaths.end());
}
}
}
}
if (mAbort) {
break;
}
// Collect pads.
ClipperLib::Paths thermalPadAreas;
ClipperLib::Paths thermalPadAreasShrinked;
ClipperLib::Paths thermalPadClearanceAreas;
foreach (const PadData& pad, data->pads) {
const bool sameNet = it->netSignal && (pad.netSignal == it->netSignal);
foreach (const PadGeometry& geometry, pad.geometries.value(it->layer)) {
if (sameNet) {
// Same net signal -> memorize as connected area.
const QVector<Path> paths =
pad.transform.map(geometry.toOutlines());
const ClipperLib::Paths clipperPaths =
ClipperHelpers::convert(paths, maxArcTolerance());
connectedNetSignalAreas.insert(connectedNetSignalAreas.end(),
clipperPaths.begin(),
clipperPaths.end());
}
if ((!sameNet) ||
(it->connectStyle != BI_Plane::ConnectStyle::Solid)) {
// Determine required clearance. For connection style 'none' for
// pads of the same net, use the thermal gap clearance since usually
// it is smaller than the planes clearance, so it leads to a higher
// plane area.
const Length clearance =
std::max(sameNet ? *it->thermalGap : *it->minClearanceToCopper,
*pad.clearance);
QVector<Path> paths =
pad.transform.map(geometry.withOffset(clearance).toOutlines());
ClipperLib::Paths clipperPaths =
ClipperHelpers::convert(paths, maxArcTolerance());
// For thermal relief connection, subtract the spokes from the
// cutout.
if (sameNet &&
(it->connectStyle == BI_Plane::ConnectStyle::ThermalRelief) &&
ClipperHelpers::anyPointsInside(clipperPaths, planeOutline)) {
// Note: Make spokes *slightly* thicker to avoid them to be
// removed due to numerical inaccuary of minimum width procedure.
// Note 2: Do not accept a spoke width smaller than the minimum
// copper width because this will effectively break all thermal
// pads, see https://github.com/LibrePCB/LibrePCB/issues/1682.
const PositiveLength spokeWidth(
std::max(*it->thermalSpokeWidth, *it->minWidth) + 10);
const Length spokeLength(100000000); // Maximum spoke length.
foreach (const auto& spokeConfig,
determineThermalSpokes(geometry)) {
const Point p1 =
spokeConfig.first.rotated(pad.transform.getRotation()) +
pad.transform.getPosition();
const Point p2 =
(Point(spokeLength, 0).rotated(spokeConfig.second) +
spokeConfig.first)
.rotated(pad.transform.getRotation()) +
pad.transform.getPosition();
const ClipperLib::Paths spokePaths{ClipperHelpers::convert(
Path::obround(p1, p2, spokeWidth), maxArcTolerance())};
ClipperHelpers::subtract(clipperPaths, spokePaths,
ClipperLib::pftEvenOdd,
ClipperLib::pftNonZero); // can throw
}
// Memorize copper area for later removal of unconnected
// thermal spokes,
ClipperLib::Paths tmp = ClipperHelpers::convert(
pad.transform.map(geometry.toOutlines()), maxArcTolerance());
if (tmp.size() > 1) {
ClipperHelpers::unite(tmp,
ClipperLib::pftNonZero); // can throw
}
thermalPadAreas.insert(thermalPadAreas.end(), tmp.begin(),
tmp.end());
// Memorize clearance area for later removal of unconnected
// thermal spokes,
Length offset = clearance + it->minWidth - maxArcTolerance() - 10;
tmp = ClipperHelpers::convert(
pad.transform.map(geometry.withOffset(offset).toOutlines()),
maxArcTolerance());
if (tmp.size() > 1) {
ClipperHelpers::unite(tmp,
ClipperLib::pftNonZero); // can throw
}
thermalPadClearanceAreas.insert(thermalPadClearanceAreas.end(),
tmp.begin(), tmp.end());
// Memorize slightly shrinked copper area for later removal of
// unconnected thermal spokes,
offset = -maxArcTolerance() - 10;
tmp = ClipperHelpers::convert(
pad.transform.map(geometry.withOffset(offset).toOutlines()),
maxArcTolerance());
thermalPadAreasShrinked.insert(thermalPadAreasShrinked.end(),
tmp.begin(), tmp.end());
}
removedAreas.insert(removedAreas.end(), clipperPaths.begin(),
clipperPaths.end());
// Also create cut-outs for each hole to ensure correct clearance
// even if the pad outline is too small or invalid.
if (!sameNet) {
for (const PadHole& hole : geometry.getHoles()) {
const PositiveLength width(hole.getDiameter() +
(clearance * 2));
paths =
pad.transform.map(hole.getPath()->toOutlineStrokes(width));
clipperPaths =
ClipperHelpers::convert(paths, maxArcTolerance());
removedAreas.insert(removedAreas.end(), clipperPaths.begin(),
clipperPaths.end());
}
}
}
}
if (mAbort) {
break;
}
}
if (mAbort) {
break;
}
// Subtract all the collected areas to remove.
ClipperHelpers::subtract(fragments, removedAreas, ClipperLib::pftEvenOdd,
ClipperLib::pftNonZero);
if (mAbort) {
break;
}
// Ensure minimum width. Reduce minWidth by 1nm to ensure plane areas
// do not disappear between two objects with a distance of *exactly*
// 2*minClearance+minWidth (e.g. two 0.5mm traces on a 1.0mm grid).
const Length minWidthOffset = (it->minWidth / 2) - 1;
if (minWidthOffset > 0) {
ClipperHelpers::offset(fragments, -minWidthOffset,
maxArcTolerance()); // can throw
ClipperHelpers::offset(fragments, minWidthOffset,
maxArcTolerance()); // can throw
}
if (mAbort) {
break;
}
// Split thermal spokes and flatten result for detecting unconnected
// thermal spokes.
std::unique_ptr<ClipperLib::PolyTree> tree =
ClipperHelpers::subtractToTree(fragments, thermalPadAreasShrinked,
ClipperLib::pftEvenOdd,
ClipperLib::pftNonZero); // can throw
fragments = ClipperHelpers::flattenTree(*tree); // can throw
if (mAbort) {
break;
}
// Remove unconnected thermal spokes.
if (thermalPadAreas.size() != thermalPadClearanceAreas.size()) {
throw LogicError(
__FILE__, __LINE__,
"Thermal pads inconsistency, please open a bug report.");
}
auto isUnconnectedSpoke = [&](const ClipperLib::Path& fragment) {
std::optional<std::size_t> padIndex;
for (std::size_t i = 0; i < thermalPadAreas.size(); ++i) {
if (ClipperHelpers::anyPointsInside(fragment,
thermalPadAreas.at(i))) {
if (padIndex) {
return false;
} else {
padIndex = i;
}
}
}
return padIndex &&
ClipperHelpers::allPointsInside(
fragment, thermalPadClearanceAreas.at(*padIndex));
};
fragments.erase(std::remove_if(fragments.begin(), fragments.end(),
isUnconnectedSpoke),
fragments.end());
if (mAbort) {
break;
}
// Fill thermal pads.
ClipperHelpers::intersect(thermalPadAreas, fullPlaneArea,
ClipperLib::pftNonZero,
ClipperLib::pftEvenOdd); // can throw
tree = ClipperHelpers::uniteToTree(fragments, thermalPadAreas,
ClipperLib::pftEvenOdd,
ClipperLib::pftNonZero); // can throw
fragments = ClipperHelpers::flattenTree(*tree); // can throw
if (mAbort) {
break;
}
// If requested, remove unconnected fragments (islands).
if (it->netSignal && (!it->keepIslands)) {
auto isIsland = [&](const ClipperLib::Path& p) {
ClipperLib::Paths intersections{p};
ClipperHelpers::intersect(intersections, connectedNetSignalAreas,
ClipperLib::pftNonZero,
ClipperLib::pftNonZero); // can throw
return intersections.empty();
};
fragments.erase(
std::remove_if(fragments.begin(), fragments.end(), isIsland),
fragments.end());
}
if (mAbort) {
break;
}
// Make result canonical for a reproducible output by rotating and
// sorting the fragments.
auto cmp = [](const ClipperLib::IntPoint& a,
const ClipperLib::IntPoint& b) {
return (a.X < b.X) || ((a.X == b.X) && (a.Y < b.Y));
};
for (ClipperLib::Path& path : fragments) {
Q_ASSERT(!path.empty());
auto minIt = std::min_element(path.begin(), path.end(), cmp);
std::rotate(path.begin(), minIt, path.end());
}
std::sort(fragments.begin(), fragments.end(),
[&cmp](const ClipperLib::Path& a, const ClipperLib::Path& b) {
return cmp(a.front(), b.front());
});
if (mAbort) {
break;
}
// Memorize fragments for this plane.
result.planes[it->uuid] = ClipperHelpers::convert(fragments);
} catch (const Exception& e) {
qCritical() << "Failed to calculate plane areas, leaving empty:"
<< e.getMsg();
result.errors.append(e.getMsg());
}
}
return result;
}
QVector<std::pair<Point, Angle>>
BoardPlaneFragmentsBuilder::determineThermalSpokes(
const PadGeometry& geometry) noexcept {
// For circular pads, rotate spokes by 45° since this often allows to
// add more spokes if several pads are placed in a row.
const bool isCircularRound =
((geometry.getShape() == PadGeometry::Shape::RoundedRect) ||
(geometry.getShape() == PadGeometry::Shape::RoundedOctagon)) &&
(geometry.getWidth() == geometry.getHeight()) &&
(geometry.getCornerRadius() >= (geometry.getWidth() / 2));
const bool isCircularStroke =
(geometry.getShape() == PadGeometry::Shape::Stroke) &&
(geometry.getPath().getVertices().count() == 1);
if (isCircularRound || isCircularStroke) {
const Point center = isCircularStroke
? geometry.getPath().getVertices().first().getPos()
: Point(0, 0);
return {
std::make_pair(center, Angle::deg45()),
std::make_pair(center, Angle::deg135()),
std::make_pair(center, Angle::deg225()),
std::make_pair(center, Angle::deg315()),
};
}
// For any shape other than a complex stroke, add horizontal and vertical
// spokes.
const bool isCenteredShape =
(geometry.getShape() != PadGeometry::Shape::Stroke);
const bool isObroundStroke =
((geometry.getShape() == PadGeometry::Shape::Stroke) &&
(geometry.getPath().getVertices().count() == 2) &&
(geometry.getPath().getVertices().first().getAngle() == Angle::deg0()));
if (isCenteredShape || isObroundStroke) {
Point center(0, 0);
Angle angle(0);
if (isObroundStroke) {
const Point p1 = geometry.getPath().getVertices().first().getPos();
const Point p2 = geometry.getPath().getVertices().last().getPos();
center = (p1 + p2) / 2;
angle = Toolbox::angleBetweenPoints(p1, p2);
}
return {
std::make_pair(center, angle),
std::make_pair(center, angle + Angle::deg90()),
std::make_pair(center, angle + Angle::deg180()),
std::make_pair(center, angle + Angle::deg270()),
};
}
// For complex strokes, add two 45° spokes on each end.
if (geometry.getPath().getVertices().count() > 1) {
const Point p1 = geometry.getPath().getVertices().first().getPos();
const Point p2 = geometry.getPath().getVertices().last().getPos();
const Angle angle = Toolbox::angleBetweenPoints(p1, p2);
return {
std::make_pair(p1, angle + Angle::deg135()),
std::make_pair(p1, angle - Angle::deg135()),
std::make_pair(p2, angle + Angle::deg45()),
std::make_pair(p2, angle - Angle::deg45()),
};
}
// For invalid strokes, add no spokes at all.
return {};
}
/*******************************************************************************
* End of File
******************************************************************************/
} // namespace librepcb