Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
0e185510cThis 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 : std::as_const(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 shrunk 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 thermalPadAreasShrunk;
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 shrunk copper area for later removal of
// unconnected thermal spokes,
offset = -maxArcTolerance() - 10;
tmp = ClipperHelpers::convert(
pad.transform.map(geometry.withOffset(offset).toOutlines()),
maxArcTolerance());
thermalPadAreasShrunk.insert(thermalPadAreasShrunk.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, thermalPadAreasShrunk,
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