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 "board2dtab.h"
#include "../../dialogs/backgroundimagesetupdialog.h"
#include "../../dialogs/filedialog.h"
#include "../../editorcommandset.h"
#include "../../graphics/graphicslayerlist.h"
#include "../../graphics/graphicslayersmodel.h"
#include "../../graphics/slintgraphicsview.h"
#include "../../guiapplication.h"
#include "../../library/libraryelementcache.h"
#include "../../library/pkg/footprintgraphicsitem.h"
#include "../../undostack.h"
#include "../../utils/editortoolbox.h"
#include "../../utils/slinthelpers.h"
#include "../../utils/uihelpers.h"
#include "../../workspace/desktopservices.h"
#include "../cmd/cmdadddevicetoboard.h"
#include "../cmd/cmdboardedit.h"
#include "../cmd/cmdboardspecctraimport.h"
#include "../projectcrossprobe.h"
#include "../projecteditor.h"
#include "boardeditor.h"
#include "boardgraphicsscene.h"
#include "boardpickplacegeneratordialog.h"
#include "fsm/boardeditorfsm.h"
#include "fsm/boardeditorstate_addhole.h"
#include "fsm/boardeditorstate_addpad.h"
#include "fsm/boardeditorstate_addstroketext.h"
#include "fsm/boardeditorstate_addvia.h"
#include "fsm/boardeditorstate_drawplane.h"
#include "fsm/boardeditorstate_drawpolygon.h"
#include "fsm/boardeditorstate_drawtrace.h"
#include "fsm/boardeditorstate_drawzone.h"
#include "graphicsitems/bgi_device.h"
#include "graphicsitems/bgi_netline.h"
#include "graphicsitems/bgi_pad.h"
#include "graphicsitems/bgi_plane.h"
#include "graphicsitems/bgi_via.h"
#include <librepcb/core/application.h>
#include <librepcb/core/attribute/attributesubstitutor.h>
#include <librepcb/core/fileio/fileutils.h>
#include <librepcb/core/fileio/transactionaldirectory.h>
#include <librepcb/core/fileio/transactionalfilesystem.h>
#include <librepcb/core/job/gerberexcellonoutputjob.h>
#include <librepcb/core/library/cmp/component.h>
#include <librepcb/core/library/dev/device.h>
#include <librepcb/core/library/org/organization.h>
#include <librepcb/core/library/pkg/package.h>
#include <librepcb/core/project/board/board.h>
#include <librepcb/core/project/board/boardd356netlistexport.h>
#include <librepcb/core/project/board/boarddesignrules.h>
#include <librepcb/core/project/board/boardpainter.h>
#include <librepcb/core/project/board/boardplanefragmentsbuilder.h>
#include <librepcb/core/project/board/boardspecctraexport.h>
#include <librepcb/core/project/board/items/bi_device.h>
#include <librepcb/core/project/board/items/bi_netline.h>
#include <librepcb/core/project/board/items/bi_netsegment.h>
#include <librepcb/core/project/board/items/bi_pad.h>
#include <librepcb/core/project/board/items/bi_plane.h>
#include <librepcb/core/project/board/items/bi_via.h>
#include <librepcb/core/project/circuit/circuit.h>
#include <librepcb/core/project/circuit/componentinstance.h>
#include <librepcb/core/project/circuit/netclass.h>
#include <librepcb/core/project/circuit/netsignal.h>
#include <librepcb/core/project/project.h>
#include <librepcb/core/project/projectattributelookup.h>
#include <librepcb/core/project/projectlibrary.h>
#include <librepcb/core/types/layer.h>
#include <librepcb/core/utils/messagelogger.h>
#include <librepcb/core/utils/scopeguard.h>
#include <librepcb/core/utils/tagmatcher.h>
#include <librepcb/core/workspace/colorrole.h>
#include <librepcb/core/workspace/workspace.h>
#include <librepcb/core/workspace/workspacelibrarydb.h>
#include <librepcb/core/workspace/workspacesettings.h>
#include <QtCore>
#include <QtWidgets>
#include <memory>
/*******************************************************************************
* Namespace
******************************************************************************/
namespace librepcb {
namespace editor {
static ui::WireMode l2s(BoardEditorState_DrawTrace::WireMode v) noexcept {
if (v == BoardEditorState_DrawTrace::WireMode::HV) {
return ui::WireMode::HV;
} else if (v == BoardEditorState_DrawTrace::WireMode::VH) {
return ui::WireMode::VH;
} else if (v == BoardEditorState_DrawTrace::WireMode::Deg9045) {
return ui::WireMode::Deg9045;
} else if (v == BoardEditorState_DrawTrace::WireMode::Deg4590) {
return ui::WireMode::Deg4590;
} else if (v == BoardEditorState_DrawTrace::WireMode::Straight) {
return ui::WireMode::Straight;
} else {
return ui::WireMode::HV;
}
}
static BoardEditorState_DrawTrace::WireMode s2l(ui::WireMode v) noexcept {
if (v == ui::WireMode::HV) {
return BoardEditorState_DrawTrace::WireMode::HV;
} else if (v == ui::WireMode::VH) {
return BoardEditorState_DrawTrace::WireMode::VH;
} else if (v == ui::WireMode::Deg9045) {
return BoardEditorState_DrawTrace::WireMode::Deg9045;
} else if (v == ui::WireMode::Deg4590) {
return BoardEditorState_DrawTrace::WireMode::Deg4590;
} else if (v == ui::WireMode::Straight) {
return BoardEditorState_DrawTrace::WireMode::Straight;
} else {
return BoardEditorState_DrawTrace::WireMode::HV;
}
}
/*******************************************************************************
* Constructors / Destructor
******************************************************************************/
Board2dTab::Board2dTab(GuiApplication& app, BoardEditor& editor,
QObject* parent) noexcept
: WindowTab(app, parent),
onDerivedUiDataChanged(*this),
mProjectEditor(editor.getProjectEditor()),
mProject(mProjectEditor.getProject()),
mBoardEditor(editor),
mBoard(mBoardEditor.getBoard()),
mLayers(GraphicsLayerList::boardLayers(&app.getWorkspace().getSettings())),
mView(new SlintGraphicsView(SlintGraphicsView::defaultBoardSceneRect(),
SlintGraphicsView::defaultEditorMargins(),
this)),
mMsgEmptySchematics(app.getWorkspace(), "EMPTY_BOARD_NO_COMPONENTS"),
mMsgSetupDesignRules(app.getWorkspace(), "EMPTY_BOARD_SETUP_DESIGN_RULES"),
mMsgPlaceDevices(app.getWorkspace(), "EMPTY_BOARD_PLACE_DEVICES"),
mGridStyle(mApp.getWorkspace().getSettings().boardGridStyle.get()),
mIgnorePlacementLocks(false),
mFrameIndex(0),
mToolFeatures(),
mTool(ui::EditorTool::Select),
mToolCursorShape(Qt::ArrowCursor),
mToolWireMode(BoardEditorState_DrawTrace::WireMode::HV),
mToolNets(std::make_shared<slint::VectorModel<slint::SharedString>>()),
mToolNet({true, std::nullopt}),
mToolLayers(std::make_shared<slint::VectorModel<slint::SharedString>>()),
mToolLayer(nullptr),
mToolLineWidth(app.getWorkspace().getSettings()),
mToolSize(app.getWorkspace().getSettings()),
mToolDrill(app.getWorkspace().getSettings()),
mToolFilled(false),
mToolMirrored(false),
mToolValueSuggestions(
std::make_shared<slint::VectorModel<slint::SharedString>>()),
mToolComponentSide(Pad::ComponentSide::Top),
mToolShape(ui::PadShape::Round),
mToolRatio(Ratio(0)),
mToolFiducial(false),
mToolPressFit(false),
mToolZoneRules(),
mUnplacedComponentIndex(0),
mUnplacedComponentDeviceIndex(0),
mUnplacedComponentPackageOwned(false),
mUnplacedComponentFootprintIndex(0),
mBackgroundImageGraphicsItem(new QGraphicsPixmapItem()) {
Q_ASSERT(&mBoard.getProject() == &mProject);
// Load/store layers visibility.
updateEnabledCopperLayers();
connect(&mBoard, &Board::innerLayerCountChanged, this,
&Board2dTab::updateEnabledCopperLayers);
loadLayersVisibility();
connect(&mProjectEditor, &ProjectEditor::projectAboutToBeSaved, this,
&Board2dTab::storeLayersVisibility);
// Reset memorized footprint selections when preferred tags have changed.
connect(&mBoard, &Board::preferredFootprintTagsChanged, this, [this]() {
mLastFootprintOfPackage.clear();
setSelectedUnplacedComponentDevice(mUnplacedComponentDeviceIndex);
});
// Setup graphics view.
mView->setEventHandler(this);
mView->setUseOpenGl(mApp.getWorkspace().getSettings().useOpenGl.get());
connect(
&mApp.getWorkspace().getSettings().useOpenGl,
&WorkspaceSettingsItem::edited, this, [this]() {
mView->setUseOpenGl(mApp.getWorkspace().getSettings().useOpenGl.get());
});
connect(mView.get(), &SlintGraphicsView::transformChanged, this,
&Board2dTab::requestRepaint);
connect(mView.get(), &SlintGraphicsView::stateChanged, this,
[this]() { onDerivedUiDataChanged.notify(); });
// Connect board editor.
connect(&mBoardEditor, &BoardEditor::uiIndexChanged, this,
[this]() { onDerivedUiDataChanged.notify(); });
connect(&mBoardEditor, &BoardEditor::planesRebuildStatusChanged, this,
[this]() { onDerivedUiDataChanged.notify(); });
connect(&mBoardEditor, &BoardEditor::planesUpdated, this,
&Board2dTab::requestRepaint);
connect(&mBoardEditor, &BoardEditor::drcMessageHighlightRequested, this,
&Board2dTab::highlightDrcMessage);
connect(&mBoardEditor, &BoardEditor::aboutToBeDestroyed, this,
&Board2dTab::closeEnforced);
// Connect project editor.
connect(&mProjectEditor, &ProjectEditor::uiIndexChanged, this,
[this]() { onDerivedUiDataChanged.notify(); });
connect(&mProjectEditor, &ProjectEditor::abortBlockingToolsInOtherEditors,
this, [this](const void* source) {
if (source != this) {
// Not so nice...
mFsm->processAbortCommand();
mFsm->processAbortCommand();
mFsm->processAbortCommand();
}
});
// Connect undo stack.
connect(&mProjectEditor.getUndoStack(), &UndoStack::stateModified, this,
[this]() { onUiDataChanged.notify(); });
connect(&mProjectEditor, &ProjectEditor::manualModificationsMade, this,
[this]() { onUiDataChanged.notify(); });
// Connect search context.
connect(&mSearchContext, &SearchContext::goToTriggered, this,
&Board2dTab::goToObjects);
// Setup messages.
connect(&mProject.getCircuit(), &Circuit::componentAdded, this,
&Board2dTab::updateMessages);
connect(&mProject.getCircuit(), &Circuit::componentRemoved, this,
&Board2dTab::updateMessages);
connect(&mBoard, &Board::deviceAdded, this, &Board2dTab::updateMessages);
connect(&mBoard, &Board::deviceRemoved, this, &Board2dTab::updateMessages);
connect(&mMsgEmptySchematics, &DismissableMessageContext::visibilityChanged,
this, [this]() { onDerivedUiDataChanged.notify(); });
connect(&mMsgSetupDesignRules, &DismissableMessageContext::visibilityChanged,
this, [this]() { onDerivedUiDataChanged.notify(); });
connect(&mMsgPlaceDevices, &DismissableMessageContext::visibilityChanged,
this, [this]() { onDerivedUiDataChanged.notify(); });
updateMessages();
// Setup background image.
mBackgroundImageGraphicsItem->setFlag(QGraphicsItem::ItemIsSelectable, true);
mBackgroundImageGraphicsItem->setTransformationMode(Qt::SmoothTransformation);
mBackgroundImageGraphicsItem->setZValue(-1000);
mBackgroundImageGraphicsItem->setOpacity(0.8);
mBackgroundImageGraphicsItem->setVisible(false);
mBackgroundImageSettings.tryLoadFromDir(getBackgroundImageCacheDir());
applyBackgroundImageSettings();
// Build the whole board editor finite state machine.
BoardEditorFsm::Context fsmContext{
mApp.getWorkspace(), mProject, mBoard,
mProjectEditor.getUndoStack(), *mLayers, *this,
};
mFsm.reset(new BoardEditorFsm(fsmContext));
// Apply workspace settings whenever they have been modified.
connect(&mApp.getWorkspace().getSettings().boardGridStyle,
&WorkspaceSettingsItem::edited, this, [this]() {
mGridStyle = mApp.getWorkspace().getSettings().boardGridStyle.get();
applyWorkspaceSettings();
});
connect(&mApp.getWorkspace().getSettings().boardColorSchemes,
&WorkspaceSettingsItem_ColorSchemes::edited, this,
&Board2dTab::applyWorkspaceSettings);
}
Board2dTab::~Board2dTab() noexcept {
clearDrcMarker(); // Avoid dangling pointers.
setSelectedUnplacedComponent(-1); // Release memory if needed.
deactivate();
mView->setEventHandler(nullptr);
// Delete FSM as it may trigger some other methods during destruction.
mFsm.reset();
}
/*******************************************************************************
* General Methods
******************************************************************************/
int Board2dTab::getProjectIndex() const noexcept {
return mProjectEditor.getUiIndex();
}
int Board2dTab::getProjectObjectIndex() const noexcept {
return mProject.getBoardIndex(mBoard);
}
ui::TabData Board2dTab::getUiData() const noexcept {
ui::TabFeatures features = {};
features.save = toFs(mProject.getDirectory().isWritable());
features.undo = toFs(mProjectEditor.getUndoStack().canUndo());
features.redo = toFs(mProjectEditor.getUndoStack().canRedo());
features.grid = toFs(mProject.getDirectory().isWritable());
features.zoom = toFs(true);
features.import_graphics =
toFs(mToolFeatures.testFlag(Feature::ImportGraphics));
features.export_graphics = toFs(mTool == ui::EditorTool::Select);
features.select = toFs(mTool == ui::EditorTool::Select);
features.cut = toFs(mToolFeatures.testFlag(Feature::Cut));
features.copy = toFs(mToolFeatures.testFlag(Feature::Copy));
features.paste = toFs(mToolFeatures.testFlag(Feature::Paste));
features.remove = toFs(mToolFeatures.testFlag(Feature::Remove));
features.rotate = toFs(mToolFeatures.testFlag(Feature::Rotate));
features.flip = toFs(mToolFeatures.testFlag(Feature::Flip));
features.snap_to_grid = toFs(mToolFeatures.testFlag(Feature::SnapToGrid));
features.reset_texts = toFs(mToolFeatures.testFlag(Feature::ResetTexts));
features.lock = toFs(mToolFeatures.testFlag(Feature::Lock));
features.unlock = toFs(mToolFeatures.testFlag(Feature::Unlock));
features.edit_properties = toFs(mToolFeatures.testFlag(Feature::Properties));
features.modify_line_width =
toFs(mToolFeatures.testFlag(Feature::ModifyLineWidth));
features.find = toFs(true);
return ui::TabData{
ui::TabType::Board2d, // Type
q2s(mBoardEditor.getDisplayName()), // Title
features, // Features
!mProject.getDirectory().isWritable(), // Read-only
mProjectEditor.hasUnsavedChanges(), // Unsaved changes
q2s(mProjectEditor.getUndoStack().getUndoCmdText()), // Undo text
q2s(mProjectEditor.getUndoStack().getRedoCmdText()), // Redo text
q2s(mSearchContext.getTerm()), // Find term
mSearchContext.getModel(), // Find suggestions
mLayersModel, // Layers
};
}
void Board2dTab::setUiData(const ui::TabData& data) noexcept {
mSearchContext.setTerm(s2q(data.find_term));
WindowTab::setUiData(data);
onUiDataChanged.notify();
}
ui::Board2dTabData Board2dTab::getDerivedUiData() const noexcept {
const ColorScheme& scheme =
mApp.getWorkspace().getSettings().boardColorSchemes.getActive();
const auto infoBoxColors = scheme.getColors(ColorRole::boardInfoBox());
const QColor bgColor = scheme.getColors(ColorRole::boardBackground()).primary;
const QColor fgColor = (bgColor.lightnessF() >= 0.5) ? Qt::black : Qt::white;
return ui::Board2dTabData{
mProjectEditor.getUiIndex(), // Project index
mBoardEditor.getUiIndex(), // Board index
q2s(bgColor), // Background color
q2s(fgColor), // Foreground color
q2s(infoBoxColors.primary), // Overlay color
q2s(infoBoxColors.secondary), // Overlay text color
l2s(mGridStyle), // Grid style
l2s(*mBoard.getGridInterval()), // Grid interval
l2s(mBoard.getGridUnit()), // Length unit
mScene->isFlipped(), // Flip view (view from bottom)
mBackgroundImageGraphicsItem->isVisible(), // Background image set
static_cast<float>(mBackgroundImageGraphicsItem->opacity()), // BG alpha
mIgnorePlacementLocks, // Ignore placement locks
mBoardEditor.isRebuildingPlanes(), // Refreshing
mMsgEmptySchematics.getUiData(), // Message "empty schematics"
mMsgSetupDesignRules.getUiData(), // Message "setup design rules"
mMsgPlaceDevices.getUiData(), // Message "place devices"
mUnplacedComponentsModel, // Unplaced components
mUnplacedComponentIndex, // Unplaced components index
mUnplacedComponentDevicesModel, // Unplaced components devices
mUnplacedComponentDeviceIndex, // Unplaced components device index
mUnplacedComponentFootprintsModel, // Unplaced components footprints
mUnplacedComponentFootprintIndex, // Unplaced components footprint index
slint::Image(), // Unplaced components preview
mTool, // Tool
q2s(mView->isPanning() ? Qt::ClosedHandCursor
: mToolCursorShape), // Tool cursor
q2s(mToolOverlayText), // Tool overlay text
l2s(mToolWireMode), // Tool wire mode
ui::ComboBoxData{
// Tool net
mToolNets, // Items,
static_cast<int>(mToolNetsQt.indexOf(mToolNet)), // Current index
},
q2s(mToolNetClassName), // Tool net class name
ui::ComboBoxData{
// Tool layer
mToolLayers, // Items
static_cast<int>(mToolLayersQt.indexOf(mToolLayer)), // Current
},
mToolLineWidth.getUiData(), // Tool line width
mToolSize.getUiData(), // Tool size
mToolDrill.getUiData(), // Tool drill
ui::AngleEditData{
// Tool angle
l2s(mToolAngle), // Angle
false, // Increase
false, // Decrease
},
mToolFilled, // Tool filled
mToolMirrored, // Tool mirrored
ui::LineEditData{
// Tool value
true, // Enabled
q2s(EditorToolbox::toSingleLine(mToolValue)), // Text
slint::SharedString(), // Placeholder
mToolValueSuggestions, // Suggestions
},
mToolComponentSide == Pad::ComponentSide::Bottom, // Tool bottom
mToolShape, // Tool shape
ui::RatioEditData{
// Tool ratio
l2s(*mToolRatio), // Ratio
l2s(Ratio::fromPercent(0)), // Minimum
l2s(Ratio::fromPercent(100)), // Maximum
(*mToolRatio) < Ratio::fromPercent(100), // Can increase
(*mToolRatio) > Ratio::fromPercent(0), // Can decrease
false, // Increase
false, // Decrease
},
mToolFiducial, // Tool fiducial
mToolPressFit, // Tool press fit
mToolZoneRules.testFlag(Zone::Rule::NoCopper), // Tool no copper
mToolZoneRules.testFlag(Zone::Rule::NoPlanes), // Tool no planes
mToolZoneRules.testFlag(Zone::Rule::NoExposure), // Tool no exposure
mToolZoneRules.testFlag(Zone::Rule::NoDevices), // Tool no devices
q2s(mSceneImagePos), // Scene image position
mFrameIndex, // Frame index
slint::SharedString(), // Set design rules organization/rules
};
}
void Board2dTab::setDerivedUiData(const ui::Board2dTabData& data) noexcept {
mSceneImagePos = s2q(data.scene_image_pos);
mGridStyle = s2l(data.grid_style);
const std::optional<PositiveLength> interval = s2plength(data.grid_interval);
if (interval && (*interval != mBoard.getGridInterval())) {
mBoard.setGridInterval(*interval);
mProjectEditor.setManualModificationsMade();
}
if (mScene) {
mScene->setGridStyle(mGridStyle);
mScene->setGridInterval(mBoard.getGridInterval());
}
const LengthUnit unit = s2l(data.unit);
if (unit != mBoard.getGridUnit()) {
mBoard.setGridUnit(unit);
mProjectEditor.setManualModificationsMade();
}
// Flip view (view from bottom)
mScene->setFlipped(data.flip_view);
mView->setMirror(data.flip_view);
// Background image
mBackgroundImageGraphicsItem->setOpacity(data.background_image_alpha);
// Placement locks
mIgnorePlacementLocks = data.ignore_placement_locks;
// Messages
mMsgEmptySchematics.setUiData(data.empty_schematics_msg);
mMsgSetupDesignRules.setUiData(data.setup_design_rules_msg);
mMsgPlaceDevices.setUiData(data.place_devices_msg);
// Unplaced component index
if (data.unplaced_components_index != mUnplacedComponentIndex) {
setSelectedUnplacedComponent(data.unplaced_components_index);
} else if (data.unplaced_components_devices_index !=
mUnplacedComponentDeviceIndex) {
setSelectedUnplacedComponentDevice(data.unplaced_components_devices_index);
} else if (data.unplaced_components_footprints_index !=
mUnplacedComponentFootprintIndex) {
setSelectedUnplacedComponentFootprint(
data.unplaced_components_footprints_index);
}
// Tool net
const auto netCfg = mToolNetsQt.value(data.tool_net.current_index);
emit netRequested(netCfg.first, netCfg.second);
// Tool layer
if (const Layer* layer = mToolLayersQt.value(data.tool_layer.current_index)) {
emit layerRequested(*layer);
}
// Tool wire mode
emit wireModeRequested(s2l(data.tool_wire_mode));
// Tool line width
mToolLineWidth.setUiData(data.tool_line_width);
// Tool size
mToolSize.setUiData(data.tool_size);
// Tool via size
// Note: We set the via size before (via) drill to let the FSM increase the
// size if drill is set to a larger value. This clipping does not work in both
// directions yet because we don't know if the user edited the drill or size.
const Length size = mToolSize.getValue();
emit viaSizeRequested(((size > 0) && (!data.tool_mirrored))
? std::make_optional(PositiveLength(size))
: std::nullopt);
// Tool drill
mToolDrill.setUiData(data.tool_drill);
emit viaDrillRequested(
((mToolDrill.getValue() > 0) && (!data.tool_pressfit))
? std::make_optional(PositiveLength(mToolDrill.getValue()))
: std::nullopt);
// Tool angle
if (data.tool_angle.increase) {
emit angleRequested(mToolAngle + Angle::deg45());
} else if (data.tool_angle.decrease) {
emit angleRequested(mToolAngle - Angle::deg45());
} else {
emit angleRequested(s2angle(data.tool_angle.value));
}
// Tool filled / auto-width
emit filledRequested(data.tool_filled);
// Tool mirrored / auto-size
emit mirroredRequested(data.tool_mirrored);
// Tool value
emit valueRequested(EditorToolbox::toMultiLine(s2q(data.tool_value.text)));
// Tool component side
emit componentSideRequested(data.tool_bottom ? Pad::ComponentSide::Bottom
: Pad::ComponentSide::Top);
// Tool ratio (must be emiteed *before* the shape!)
if (data.tool_ratio.increase) {
emit ratioRequested(UnsignedLimitedRatio(std::min(
*mToolRatio + Ratio::fromPercent(1), Ratio::fromPercent(100))));
} else if (data.tool_ratio.decrease) {
emit ratioRequested(UnsignedLimitedRatio(
std::max(*mToolRatio - Ratio::fromPercent(1), Ratio::fromPercent(0))));
} else {
const Ratio ratio = s2ratio(data.tool_ratio.value);
if ((ratio >= Ratio::fromPercent(0)) &&
(ratio <= Ratio::fromPercent(100))) {
emit ratioRequested(UnsignedLimitedRatio(ratio));
}
}
// Tool shape
emit shapeRequested(data.tool_shape);
// Tool press-fit
emit pressFitRequested(data.tool_pressfit);
// Tool zone rules
emit zoneRuleRequested(Zone::Rule::NoCopper, data.tool_no_copper);
emit zoneRuleRequested(Zone::Rule::NoPlanes, data.tool_no_planes);
emit zoneRuleRequested(Zone::Rule::NoExposure, data.tool_no_exposures);
emit zoneRuleRequested(Zone::Rule::NoDevices, data.tool_no_devices);
// Set design rules
if (!data.set_design_rules.empty()) {
loadDesignRules(s2q(data.set_design_rules));
}
requestRepaint();
}
void Board2dTab::activate() noexcept {
mLayersModel = std::make_shared<GraphicsLayersModel>(*mLayers);
connect(mLayersModel.get(), &GraphicsLayersModel::layersVisibilityChanged,
&mBoardEditor, &BoardEditor::schedulePlanesRebuild);
mScene = std::make_unique<BoardGraphicsScene>(
mBoard, *mLayers,
std::make_shared<BoardGraphicsScene::Context>(BoardGraphicsScene::Context{
this, // tab
mProjectEditor.getCrossProbe(), // cross probe
GraphicsLayer::State::Highlighted, // Self-probe mode
false, // flip view
}),
this);
mScene->setGridInterval(mBoard.getGridInterval());
connect(mScene.get(), &GraphicsScene::changed, this,
&Board2dTab::requestRepaint);
mScene->addItem(*mBackgroundImageGraphicsItem);
// Force airwire rebuild immediately and on every project modification.
mBoard.triggerAirWiresRebuild();
mActiveConnections.append(connect(&mProjectEditor.getUndoStack(),
&UndoStack::stateModified, &mBoard,
&Board::triggerAirWiresRebuild));
// Unplaced component state.
mUnplacedComponentsModel =
std::make_shared<slint::VectorModel<slint::SharedString>>();
mUnplacedComponentDevicesModel =
std::make_shared<slint::VectorModel<slint::SharedString>>();
mUnplacedComponentFootprintsModel =
std::make_shared<slint::VectorModel<slint::SharedString>>();
mUnplacedComponentGraphicsScene = std::make_unique<GraphicsScene>();
mUnplacedComponentGraphicsScene->setOriginCrossVisible(false);
mUnplacedComponentGraphicsItem.reset();
// Update unplaced components when needed.
mUnplacedComponentsUpdateTimer = std::make_unique<QTimer>(this);
mUnplacedComponentsUpdateTimer->setSingleShot(true);
connect(mUnplacedComponentsUpdateTimer.get(), &QTimer::timeout, this,
&Board2dTab::updateUnplacedComponents);
connect(&mProject.getCircuit(), &Circuit::componentAdded, this,
&Board2dTab::scheduleUnplacedComponentsUpdate);
connect(&mProject.getCircuit(), &Circuit::componentRemoved, this,
&Board2dTab::scheduleUnplacedComponentsUpdate);
mActiveConnections.append(
connect(&mBoard, &Board::deviceAdded, this,
&Board2dTab::scheduleUnplacedComponentsUpdate));
mActiveConnections.append(
connect(&mBoard, &Board::deviceRemoved, this,
&Board2dTab::scheduleUnplacedComponentsUpdate));
scheduleUnplacedComponentsUpdate();
// Initialize search context.
mSearchContext.init();
// Setup input idle timer for planes rebuilding during commands.
mInputIdleTimer = std::make_unique<QTimer>();
mInputIdleTimer->setInterval(700);
mInputIdleTimer->setSingleShot(true);
connect(mInputIdleTimer.get(), &QTimer::timeout, &mBoardEditor,
&BoardEditor::schedulePlanesRebuild);
applyWorkspaceSettings();
mBoardEditor.registerActiveTab(this);
requestRepaint();
}
void Board2dTab::deactivate() noexcept {
mInputIdleTimer.reset();
mBoardEditor.unregisterActiveTab(this);
while (!mActiveConnections.isEmpty()) {
disconnect(mActiveConnections.takeLast());
}
mSearchContext.deinit();
mUnplacedComponentGraphicsItem.reset();
mUnplacedComponentGraphicsScene.reset();
mUnplacedComponentFootprintsModel.reset();
mUnplacedComponentDevicesModel.reset();
mUnplacedComponentsModel.reset();
mDrcLocationGraphicsItem.reset();
if (mScene && (mBackgroundImageGraphicsItem->scene() == mScene.get())) {
mScene->removeItem(*mBackgroundImageGraphicsItem);
}
mScene.reset();
mLayersModel.reset();
}
void Board2dTab::trigger(ui::TabAction a) noexcept {
mProjectEditor.setCurrentTab(this);
restartIdleTimer();
switch (a) {
case ui::TabAction::Print: {
execGraphicsExportDialog(GraphicsExportDialog::Output::Print, "print");
break;
}
case ui::TabAction::ExportImage: {
execGraphicsExportDialog(GraphicsExportDialog::Output::Image,
"image_export");
break;
}
case ui::TabAction::ExportPdf: {
execGraphicsExportDialog(GraphicsExportDialog::Output::Pdf, "pdf_export");
break;
}
case ui::TabAction::BillOfMaterials: {
mProjectEditor.execBomReviewDialog(&mBoard);
break;
}
case ui::TabAction::ExportFabricationData: {
mProjectEditor.execOutputJobsDialog(
GerberExcellonOutputJob::getTypeName());
break;
}
case ui::TabAction::ExportPickPlace: {
execPickPlaceExportDialog();
break;
}
case ui::TabAction::ExportD356Netlist: {
execD356NetlistExportDialog();
break;
}
case ui::TabAction::ExportSpecctra: {
execSpecctraExportDialog();
break;
}
case ui::TabAction::ImportSpecctra: {
execSpecctraImportDialog();
break;
}
case ui::TabAction::ImportDxf: {
mFsm->processImportDxf();
break;
}
case ui::TabAction::PlanesHide: {
foreach (BI_Plane* p, mBoard.getPlanes()) {
p->setVisible(false); // No undo command needed since it is not saved.
}
break;
}
case ui::TabAction::PlanesShow: {
foreach (BI_Plane* p, mBoard.getPlanes()) {
p->setVisible(true); // No undo command needed since it is not saved.
}
break;
}
case ui::TabAction::PlanesRebuild: {
mBoardEditor.startPlanesRebuild(true);
break;
}
case ui::TabAction::SelectAll: {
mFsm->processSelectAll();
break;
}
case ui::TabAction::Abort: {
if (mDrcLocationGraphicsItem) {
clearDrcMarker();
} else {
mFsm->processAbortCommand();
}
break;
}
case ui::TabAction::Save: {
mProjectEditor.saveProject();
break;
}
case ui::TabAction::Undo: {
mProjectEditor.undo();
break;
}
case ui::TabAction::Redo: {
mProjectEditor.redo();
break;
}
case ui::TabAction::Cut: {
mFsm->processCut();
break;
}
case ui::TabAction::Copy: {
mFsm->processCopy();
break;
}
case ui::TabAction::Paste: {
mFsm->processPaste();
break;
}
case ui::TabAction::Delete: {
mFsm->processRemove();
break;
}
case ui::TabAction::RotateCcw: {
mFsm->processRotate(Angle::deg90());
break;
}
case ui::TabAction::RotateCw: {
mFsm->processRotate(-Angle::deg90());
break;
}
case ui::TabAction::FlipHorizontally: {
mFsm->processFlip(Qt::Horizontal);
break;
}
case ui::TabAction::FlipVertically: {
mFsm->processFlip(Qt::Vertical);
break;
}
case ui::TabAction::MoveLeft: {
if (!mFsm->processMove(Point(-mBoard.getGridInterval(), 0))) {
mView->scrollLeft();
}
break;
}
case ui::TabAction::MoveRight: {
if (!mFsm->processMove(Point(*mBoard.getGridInterval(), 0))) {
mView->scrollRight();
}
break;
}
case ui::TabAction::MoveUp: {
if (!mFsm->processMove(Point(0, *mBoard.getGridInterval()))) {
mView->scrollUp();
}
break;
}
case ui::TabAction::MoveDown: {
if (!mFsm->processMove(Point(0, -mBoard.getGridInterval()))) {
mView->scrollDown();
}
break;
}
case ui::TabAction::SnapToGrid: {
mFsm->processSnapToGrid();
break;
}
case ui::TabAction::Lock: {
mFsm->processSetLocked(true);
break;
}
case ui::TabAction::Unlock: {
mFsm->processSetLocked(false);
break;
}
case ui::TabAction::LineWidthIncrease: {
mFsm->processChangeLineWidth(1);
break;
}
case ui::TabAction::LineWidthDecrease: {
mFsm->processChangeLineWidth(-1);
break;
}
case ui::TabAction::LineWidthSet: {
mFsm->processChangeLineWidth(0);
break;
}
case ui::TabAction::ResetTexts: {
mFsm->processResetAllTexts();
break;
}
case ui::TabAction::EditProperties: {
mFsm->processEditProperties();
break;
}
case ui::TabAction::GridIntervalIncrease: {
mBoard.setGridInterval(PositiveLength(mBoard.getGridInterval() * 2));
if (mScene) {
mScene->setGridInterval(mBoard.getGridInterval());
requestRepaint();
}
break;
}
case ui::TabAction::GridIntervalDecrease: {
if ((*mBoard.getGridInterval() % 2) == 0) {
mBoard.setGridInterval(PositiveLength(mBoard.getGridInterval() / 2));
if (mScene) {
mScene->setGridInterval(mBoard.getGridInterval());
requestRepaint();
}
}
break;
}
case ui::TabAction::LayersTop: {
mLayers->showTop();
break;
}
case ui::TabAction::LayersBottom: {
mLayers->showBottom();
break;
}
case ui::TabAction::LayersTopBottom: {
mLayers->showTopAndBottom();
break;
}
case ui::TabAction::LayersAll: {
mLayers->showAll();
break;
}
case ui::TabAction::LayersNone: {
mLayers->showNone();
break;
}
case ui::TabAction::ZoomIn: {
mView->zoomIn();
break;
}
case ui::TabAction::ZoomOut: {
mView->zoomOut();
break;
}
case ui::TabAction::ZoomFit: {
if (mScene) mView->zoomToSceneRect(mScene->itemsBoundingRect(), true);
break;
}
case ui::TabAction::ToggleBackgroundImage: {
toggleBackgroundImage();
break;
}
case ui::TabAction::FindRefreshSuggestions: {
QVector<SearchContext::Candidate> objects;
for (const BI_Device* dev : mBoard.getDeviceInstances()) {
objects.append({SearchContext::ObjectType::Component,
*dev->getComponentInstance().getName()});
}
for (const NetSignal* net : mProject.getCircuit().getNetSignals()) {
if (!net->isAnonymous()) {
objects.append({SearchContext::ObjectType::Net, *net->getName()});
}
}
mSearchContext.setCandidates(objects);
break;
}
case ui::TabAction::FindNext: {
mSearchContext.findNext();
break;
}
case ui::TabAction::FindPrevious: {
mSearchContext.findPrevious();
break;
}
case ui::TabAction::BoardPlaceComponent: {
addUnplacedComponentsToBoard(PlaceComponentsMode::Single);
break;
}
case ui::TabAction::BoardPlaceComponentsSimilar: {
addUnplacedComponentsToBoard(PlaceComponentsMode::Similar);
break;
}
case ui::TabAction::BoardPlaceComponentsAll: {
addUnplacedComponentsToBoard(PlaceComponentsMode::All);
break;
}
case ui::TabAction::BoardAddPlane: {
emit autoAddPlaneRequested(); // Connected to current FSM state.
break;
}
case ui::TabAction::ToolSelect: {
mFsm->processSelect();
break;
}
case ui::TabAction::ToolWire: {
mFsm->processDrawTrace();
break;
}
case ui::TabAction::ToolVia: {
mFsm->processAddVia();
break;
}
case ui::TabAction::ToolPadTht: {
mFsm->processAddThtPad();
break;
}
case ui::TabAction::ToolPadSmt: {
mFsm->processAddSmtPad(Pad::Function::StandardPad);
break;
}
case ui::TabAction::ToolPadThermal: {
mFsm->processAddSmtPad(Pad::Function::ThermalPad);
break;
}
case ui::TabAction::ToolPadBga: {
mFsm->processAddSmtPad(Pad::Function::BgaPad);
break;
}
case ui::TabAction::ToolPadEdgeConnector: {
mFsm->processAddSmtPad(Pad::Function::EdgeConnectorPad);
break;
}
case ui::TabAction::ToolPadTestPoint: {
mFsm->processAddSmtPad(Pad::Function::TestPad);
break;
}
case ui::TabAction::ToolPadLocalFiducial: {
mFsm->processAddSmtPad(Pad::Function::LocalFiducial);
break;
}
case ui::TabAction::ToolPadGlobalFiducial: {
mFsm->processAddSmtPad(Pad::Function::GlobalFiducial);
break;
}
case ui::TabAction::ToolPolygon: {
mFsm->processDrawPolygon();
break;
}
case ui::TabAction::ToolText: {
mFsm->processAddStrokeText();
break;
}
case ui::TabAction::ToolPlane: {
mFsm->processDrawPlane();
break;
}
case ui::TabAction::ToolZone: {
mFsm->processDrawZone();
break;
}
case ui::TabAction::ToolHole: {
mFsm->processAddHole();
break;
}
case ui::TabAction::ToolMeasure: {
mFsm->processMeasure();
break;
}
case ui::TabAction::ToolbarTraceWidthSaveInBoard: {
emit saveTraceWidthInBoardRequested();
break;
}
case ui::TabAction::ToolbarTraceWidthSaveInNetclass: {
emit saveTraceWidthInNetClassRequested();
break;
}
case ui::TabAction::ToolbarViaDrillSaveInBoard: {
emit saveViaDrillInBoardRequested();
break;
}
case ui::TabAction::ToolbarViaDrillSaveInNetclass: {
emit saveViaDrillInNetClassRequested();
break;
}
default: {
WindowTab::trigger(a);
break;
}
}
}
slint::Image Board2dTab::renderScene(float width, float height,
int scene) noexcept {
if (scene == 1) {
if (mUnplacedComponentGraphicsScene) {
SlintGraphicsView view(SlintGraphicsView::defaultFootprintSceneRect(),
SlintGraphicsView::defaultMargins());
view.setUseOpenGl(mApp.getWorkspace().getSettings().useOpenGl.get());
return view.render(*mUnplacedComponentGraphicsScene, width, height);
} else {
QPixmap pix(width, height);
pix.fill(mApp.getWorkspace()
.getSettings()
.boardColorSchemes.getActive()
.getColors(ColorRole::boardBackground())
.primary);
return q2s(pix);
}
} else if (mScene) {
return mView->render(*mScene, width, height);
} else {
return slint::Image();
}
}
void Board2dTab::processScenePointerEvent(const QPointF& pos,
slint::private_api::PointerEvent e,
int scene) noexcept {
Q_UNUSED(scene);
mProjectEditor.setCurrentTab(this);
mView->pointerEvent(pos, e);
restartIdleTimer();
}
bool Board2dTab::processSceneScrolled(const QPointF& pos,
slint::private_api::PointerScrollEvent e,
int scene) noexcept {
Q_UNUSED(scene);
mProjectEditor.setCurrentTab(this);
return mView->scrollEvent(pos, e);
}
bool Board2dTab::processSceneKeyPressed(
const slint::language::KeyEvent& e) noexcept {
mProjectEditor.setCurrentTab(this);
if (mView->keyPressed(e)) {
restartIdleTimer();
return true;
}
return false;
}
bool Board2dTab::processSceneKeyReleased(
const slint::language::KeyEvent& e) noexcept {
mProjectEditor.setCurrentTab(this);
if (mView->keyReleased(e)) {
restartIdleTimer();
return true;
}
return false;
}
QSet<const Layer*> Board2dTab::getVisibleCopperLayers() const noexcept {
QSet<const Layer*> layers;
foreach (const Layer* layer, mBoard.getCopperLayers()) {
if (auto graphicsLayer = mLayers->get(*layer)) {
if (graphicsLayer->isVisible()) {
layers.insert(layer);
}
}
}
return layers;
}
/*******************************************************************************
* IF_GraphicsViewEventHandler Methods
******************************************************************************/
bool Board2dTab::graphicsSceneKeyPressed(
const GraphicsSceneKeyEvent& e) noexcept {
return mFsm->processKeyPressed(e);
}
bool Board2dTab::graphicsSceneKeyReleased(
const GraphicsSceneKeyEvent& e) noexcept {
return mFsm->processKeyReleased(e);
}
bool Board2dTab::graphicsSceneMouseMoved(
const GraphicsSceneMouseEvent& e) noexcept {
emit cursorCoordinatesChanged(e.scenePos, mBoard.getGridUnit());
return mFsm->processGraphicsSceneMouseMoved(e);
}
bool Board2dTab::graphicsSceneLeftMouseButtonPressed(
const GraphicsSceneMouseEvent& e) noexcept {
return mFsm->processGraphicsSceneLeftMouseButtonPressed(e);
}
bool Board2dTab::graphicsSceneLeftMouseButtonReleased(
const GraphicsSceneMouseEvent& e) noexcept {
return mFsm->processGraphicsSceneLeftMouseButtonReleased(e);
}
bool Board2dTab::graphicsSceneLeftMouseButtonDoubleClicked(
const GraphicsSceneMouseEvent& e) noexcept {
return mFsm->processGraphicsSceneLeftMouseButtonDoubleClicked(e);
}
bool Board2dTab::graphicsSceneRightMouseButtonReleased(
const GraphicsSceneMouseEvent& e) noexcept {
return mFsm->processGraphicsSceneRightMouseButtonReleased(e);
}
/*******************************************************************************
* BoardEditorFsmAdapter Methods
******************************************************************************/
QWidget* Board2dTab::fsmGetParentWidget() noexcept {
return getWindow();
}
BoardGraphicsScene* Board2dTab::fsmGetGraphicsScene() noexcept {
return mScene.get();
}
bool Board2dTab::fsmGetIgnoreLocks() const noexcept {
return mIgnorePlacementLocks;
}
void Board2dTab::fsmSetViewCursor(
const std::optional<Qt::CursorShape>& shape) noexcept {
if (shape) {
mToolCursorShape = *shape;
} else {
mToolCursorShape = Qt::ArrowCursor;
}
onDerivedUiDataChanged.notify();
}
void Board2dTab::fsmSetViewGrayOut(bool grayOut) noexcept {
if (mScene) {
mScene->setGrayOut(grayOut);
}
}
void Board2dTab::fsmSetViewInfoBoxText(const QString& text) noexcept {
QString t = text;
t.replace(" ", " ");
t.replace("<br>", "\n");
t.replace("<b>", "");
t.replace("</b>", "");
if (t != mToolOverlayText) {
mToolOverlayText = t;
onDerivedUiDataChanged.notify();
}
}
void Board2dTab::fsmSetViewRuler(
const std::optional<std::pair<Point, Point>>& pos) noexcept {
if (mScene) {
mScene->setRulerPositions(pos);
}
}
void Board2dTab::fsmSetSceneCursor(const Point& pos, bool cross,
bool circle) noexcept {
if (mScene) {
mScene->setSceneCursor(pos, cross, circle);
}
}
QPainterPath Board2dTab::fsmCalcPosWithTolerance(
const Point& pos, qreal multiplier) const noexcept {
return mView->calcPosWithTolerance(pos, multiplier);
}
Point Board2dTab::fsmMapGlobalPosToScenePos(const QPoint& pos) const noexcept {
if (QWidget* win = getWindow()) {
return mView->mapToScenePos(win->mapFromGlobal(pos) - mSceneImagePos,
win->devicePixelRatioF());
} else {
qWarning() << "Failed to map global position to scene position.";
return Point();
}
}
void Board2dTab::fsmCrossProbe(
const QSet<const NetSignal*>& nets,
const QSet<const ComponentInstance*>& components,
const QSet<const ComponentSignalInstance*>& cmpSignals,
GraphicsLayer::State selfProbedState) noexcept {
if (mScene && mProjectEditor.isCurrentTab(this)) {
mScene->setSelfProbedState(selfProbedState);
mProjectEditor.getCrossProbe()->set(this, nets, components, cmpSignals, {});
}
}
void Board2dTab::fsmAbortBlockingToolsInOtherEditors() noexcept {
emit mProjectEditor.abortBlockingToolsInOtherEditors(this);
}
void Board2dTab::fsmSetStatusBarMessage(const QString& message,
int timeoutMs) noexcept {
emit statusBarMessageChanged(message, timeoutMs);
}
void Board2dTab::fsmSetFeatures(Features features) noexcept {
if (features != mToolFeatures) {
mToolFeatures = features;
onUiDataChanged.notify();
}
}
void Board2dTab::fsmToolLeave() noexcept {
while (!mFsmStateConnections.isEmpty()) {
disconnect(mFsmStateConnections.takeLast());
}
mTool = ui::EditorTool::Select;
fsmSetFeatures(Features());
fsmCrossProbe(); // Reset cross-probing
onDerivedUiDataChanged.notify();
}
void Board2dTab::fsmToolEnter(BoardEditorState_Select& state) noexcept {
Q_UNUSED(state);
mTool = ui::EditorTool::Select;
onDerivedUiDataChanged.notify();
}
void Board2dTab::fsmToolEnter(BoardEditorState_DrawTrace& state) noexcept {
mTool = ui::EditorTool::Wire;
// Net class
auto setNetClass = [this](NetClass* nc) {
mToolNetClassName = nc ? *nc->getName() : QString();
onDerivedUiDataChanged.notify();
};
setNetClass(state.getNetClass());
mFsmStateConnections.append(connect(
&state, &BoardEditorState_DrawTrace::netClassChanged, this, setNetClass));
// Wire mode
auto setWireMode = [this](BoardEditorState_DrawTrace::WireMode m) {
mToolWireMode = m;
onDerivedUiDataChanged.notify();
};
setWireMode(state.getWireMode());
mFsmStateConnections.append(connect(
&state, &BoardEditorState_DrawTrace::wireModeChanged, this, setWireMode));
mFsmStateConnections.append(
connect(this, &Board2dTab::wireModeRequested, &state,
&BoardEditorState_DrawTrace::setWireMode));
// Trace width
mToolLineWidth.configure(state.getWidth(),
LengthEditContext::Steps::generic(),
"board_editor/draw_trace/width");
mFsmStateConnections.append(
connect(&state, &BoardEditorState_DrawTrace::widthChanged,
&mToolLineWidth, &LengthEditContext::setValuePositive));
mFsmStateConnections.append(
connect(&mToolLineWidth, &LengthEditContext::valueChangedPositive, &state,
&BoardEditorState_DrawTrace::setWidth));
mFsmStateConnections.append(
connect(this, &Board2dTab::saveTraceWidthInBoardRequested, &state,
&BoardEditorState_DrawTrace::saveWidthInBoard));
mFsmStateConnections.append(
connect(this, &Board2dTab::saveTraceWidthInNetClassRequested, &state,
&BoardEditorState_DrawTrace::saveWidthInNetClass));
// Auto width
auto setAutoWidth = [this](bool autoWidth) {
mToolFilled = autoWidth;
onDerivedUiDataChanged.notify();
};
setAutoWidth(state.getAutoWidth());
mFsmStateConnections.append(
connect(&state, &BoardEditorState_DrawTrace::autoWidthChanged, this,
setAutoWidth));
mFsmStateConnections.append(
connect(this, &Board2dTab::filledRequested, &state,
&BoardEditorState_DrawTrace::setAutoWidth));
// Layers
mToolLayersQt = Layer::sorted(state.getAvailableLayers());
mToolLayers->clear();
for (const Layer* layer : std::as_const(mToolLayersQt)) {
mToolLayers->push_back(q2s(layer->getNameTr()));
}
// Layer
auto setLayer = [this](const Layer& layer) {
mToolLayer = &layer;
onDerivedUiDataChanged.notify();
};
setLayer(state.getLayer());
mFsmStateConnections.append(connect(
&state, &BoardEditorState_DrawTrace::layerChanged, this, setLayer));
mFsmStateConnections.append(connect(this, &Board2dTab::layerRequested, &state,
&BoardEditorState_DrawTrace::setLayer));
// Via drill
mToolDrill.configure(state.getViaDrillDiameter(),
LengthEditContext::Steps::drillDiameter(),
"board_editor/add_via/drill"); // From via tool.
auto setViaDrill = [this](bool autoDrill, const PositiveLength& drill) {
mToolDrill.setValuePositive(drill);
mToolPressFit = autoDrill;
onDerivedUiDataChanged.notify();
};
setViaDrill(state.getViaAutoDrillDiameter(), state.getViaDrillDiameter());
mFsmStateConnections.append(
connect(&state, &BoardEditorState_DrawTrace::viaDrillDiameterChanged,
this, setViaDrill));
mFsmStateConnections.append(
connect(this, &Board2dTab::viaDrillRequested, &state,
&BoardEditorState_DrawTrace::setViaDrillDiameter));
mFsmStateConnections.append(
connect(this, &Board2dTab::saveViaDrillInBoardRequested, &state,
&BoardEditorState_DrawTrace::saveViaDrillDiameterInBoard));
mFsmStateConnections.append(
connect(this, &Board2dTab::saveViaDrillInNetClassRequested, &state,
&BoardEditorState_DrawTrace::saveViaDrillDiameterInNetClass));
// Via size
mToolSize.configure(state.getViaSize(), LengthEditContext::Steps::generic(),
"board_editor/add_via/size"); // From via tool.
auto setViaSize = [this](bool autoSize, const PositiveLength& size) {
mToolSize.setValuePositive(size);
mToolMirrored = autoSize;
onDerivedUiDataChanged.notify();
};
setViaSize(state.getAutoViaSize(), state.getViaSize());
mFsmStateConnections.append(connect(
&state, &BoardEditorState_DrawTrace::viaSizeChanged, this, setViaSize));
mFsmStateConnections.append(connect(this, &Board2dTab::viaSizeRequested,
&state,
&BoardEditorState_DrawTrace::setViaSize));
onDerivedUiDataChanged.notify();
}
void Board2dTab::fsmToolEnter(BoardEditorState_AddVia& state) noexcept {
mTool = ui::EditorTool::Via;
// Via drill
mToolDrill.configure(state.getDrillDiameter(),
LengthEditContext::Steps::drillDiameter(),
"board_editor/add_via/drill");
auto setViaDrill = [this](bool autoDrill, const PositiveLength& drill) {
mToolDrill.setValuePositive(drill);
mToolPressFit = autoDrill;
onDerivedUiDataChanged.notify();
};
setViaDrill(state.getAutoDrillDiameter(), state.getDrillDiameter());
mFsmStateConnections.append(
connect(&state, &BoardEditorState_AddVia::drillDiameterChanged, this,
setViaDrill));
mFsmStateConnections.append(
connect(this, &Board2dTab::viaDrillRequested, &state,
&BoardEditorState_AddVia::setDrillDiameter));
mFsmStateConnections.append(
connect(this, &Board2dTab::saveViaDrillInBoardRequested, &state,
&BoardEditorState_AddVia::saveDrillDiameterInBoard));
mFsmStateConnections.append(
connect(this, &Board2dTab::saveViaDrillInNetClassRequested, &state,
&BoardEditorState_AddVia::saveDrillDiameterInNetClass));
// Via size
mToolSize.configure(state.getSize(), LengthEditContext::Steps::generic(),
"board_editor/add_via/size");
auto setViaSize = [this](bool autoSize, const PositiveLength& size) {
mToolSize.setValuePositive(size);
mToolMirrored = autoSize;
onDerivedUiDataChanged.notify();
};
setViaSize(state.getUseAutoSize(), state.getSize());
mFsmStateConnections.append(
connect(&state, &BoardEditorState_AddVia::sizeChanged, this, setViaSize));
mFsmStateConnections.append(connect(this, &Board2dTab::viaSizeRequested,
&state,
&BoardEditorState_AddVia::setSize));
// Nets
mToolNetsQt.clear();
mToolNets->clear();
mToolNetsQt.append(std::make_pair(true, std::nullopt));
mToolNets->push_back(q2s("[" % tr("Auto") % "]"));
mToolNetsQt.append(std::make_pair(false, std::nullopt));
mToolNets->push_back(q2s("[" % tr("None") % "]"));
for (const auto& item : state.getAvailableNets()) {
mToolNetsQt.append(std::make_pair(false, item.first));
mToolNets->push_back(q2s(item.second));
}
// Net
auto setNet = [this](bool autoNet, const std::optional<Uuid>& net) {
if (autoNet) {
mToolNet = std::make_pair(true, std::nullopt);
} else {
mToolNet = std::make_pair(false, net);
}
mToolNetClassName.clear();
if (const NetSignal* ns =
net ? mProject.getCircuit().getNetSignals().value(*net) : nullptr) {
mToolNetClassName = *ns->getNetClass().getName();
}
onDerivedUiDataChanged.notify();
};
setNet(state.getUseAutoNet(), state.getNet());
mFsmStateConnections.append(
connect(&state, &BoardEditorState_AddVia::netChanged, this, setNet));
mFsmStateConnections.append(connect(this, &Board2dTab::netRequested, &state,
&BoardEditorState_AddVia::setNet));
onDerivedUiDataChanged.notify();
}
void Board2dTab::fsmToolEnter(BoardEditorState_AddPad& state) noexcept {
mTool = state.getType() == BoardEditorState_AddPad::PadType::THT
? ui::EditorTool::PadTht
: ui::EditorTool::PadSmt;
// Nets
mToolNetsQt.clear();
mToolNets->clear();
mToolNetsQt.append(std::make_pair(false, std::nullopt));
mToolNets->push_back(q2s("[" % tr("None") % "]"));
for (const auto& item : state.getAvailableNets()) {
mToolNetsQt.append(std::make_pair(false, item.first));
mToolNets->push_back(q2s(item.second));
}
// Net
auto setNet = [this](const std::optional<Uuid>& net) {
mToolNet = std::make_pair(false, net);
onDerivedUiDataChanged.notify();
};
setNet(state.getNet());
mFsmStateConnections.append(
connect(&state, &BoardEditorState_AddPad::netChanged, this, setNet));
mFsmStateConnections.append(
connect(this, &Board2dTab::netRequested, &state,
[&state](bool autoNet, const std::optional<Uuid>& net) {
Q_UNUSED(autoNet);
state.setNet(net);
}));
// Component side
if (state.getType() == BoardEditorState_AddPad::PadType::SMT) {
auto setComponentSide = [this](Pad::ComponentSide side) {
mToolComponentSide = side;
onDerivedUiDataChanged.notify();
};
setComponentSide(state.getComponentSide());
mFsmStateConnections.append(
connect(&state, &BoardEditorState_AddPad::componentSideChanged, this,
setComponentSide));
mFsmStateConnections.append(
connect(this, &Board2dTab::componentSideRequested, &state,
&BoardEditorState_AddPad::setComponentSide));
}
// Shape
auto getCurrentShape = [](BoardEditorState_AddPad& s) {
if (s.getShape() == Pad::Shape::RoundedOctagon) {
return ui::PadShape::Octagon;
} else if (s.getShape() == Pad::Shape::Custom) {
return ui::PadShape::Octagon; // Not correct but should never be the case
} else if (*s.getRadius() == Ratio::fromPercent(0)) {
return ui::PadShape::Rect;
} else if (*s.getRadius() == Ratio::fromPercent(100)) {
return ui::PadShape::Round;
} else {
return ui::PadShape::RoundedRect;
}
};
mToolShape = getCurrentShape(state);
mFsmStateConnections.append(connect(
this, &Board2dTab::shapeRequested, &state,
[this, &state](ui::PadShape shape) {
if (shape != mToolShape) {
switch (shape) {
case ui::PadShape::Round:
state.setShape(Pad::Shape::RoundedRect);
state.setRadius(UnsignedLimitedRatio(Ratio::fromPercent(100)));
break;
case ui::PadShape::RoundedRect:
state.setShape(Pad::Shape::RoundedRect);
state.setRadius(
UnsignedLimitedRatio(FootprintPad::getRecommendedRadius(
state.getWidth(), state.getHeight())));
break;
case ui::PadShape::Rect:
state.setShape(Pad::Shape::RoundedRect);
state.setRadius(UnsignedLimitedRatio(Ratio::fromPercent(0)));
break;
case ui::PadShape::Octagon:
state.setShape(Pad::Shape::RoundedOctagon);
state.setRadius(UnsignedLimitedRatio(Ratio::fromPercent(0)));
break;
default:
break;
}
mToolShape = shape;
}
}));
// Width / size
mToolLineWidth.configure(state.getWidth(),
LengthEditContext::Steps::generic(),
"board_editor/add_pad/width");
mFsmStateConnections.append(
connect(&state, &BoardEditorState_AddPad::widthChanged, &mToolLineWidth,
&LengthEditContext::setValuePositive));
mFsmStateConnections.append(
connect(&mToolLineWidth, &LengthEditContext::valueChangedPositive, &state,
&BoardEditorState_AddPad::setWidth));
if (state.getFunctionIsFiducial()) {
mFsmStateConnections.append(
connect(&mToolLineWidth, &LengthEditContext::valueChangedPositive,
&state, &BoardEditorState_AddPad::setHeight));
}
// Height
if (!state.getFunctionIsFiducial()) {
mToolSize.configure(state.getHeight(), LengthEditContext::Steps::generic(),
"board_editor/add_pad/height");
mFsmStateConnections.append(
connect(&state, &BoardEditorState_AddPad::heightChanged, &mToolSize,
&LengthEditContext::setValuePositive));
mFsmStateConnections.append(
connect(&mToolSize, &LengthEditContext::valueChangedPositive, &state,
&BoardEditorState_AddPad::setHeight));
}
// Fiducial clearance
const auto clearance = state.getStopMaskConfig().getOffset();
if (state.getFunctionIsFiducial() && clearance && (clearance >= 0)) {
mToolSize.configure(UnsignedLength(*clearance),
LengthEditContext::Steps::generic(),
"board_editor/add_pad/fiducial_clearance");
mFsmStateConnections.append(
connect(&state, &BoardEditorState_AddPad::copperClearanceChanged,
&mToolSize, &LengthEditContext::setValueUnsigned));
mFsmStateConnections.append(
connect(&mToolSize, &LengthEditContext::valueChangedUnsigned, &state,
[&state](const UnsignedLength& value) {
state.setCopperClearance(value);
state.setStopMaskConfig(MaskConfig::manual(*value));
}));
}
// Drill
if (auto drill = state.getDrillDiameter()) {
mToolDrill.configure(*drill, LengthEditContext::Steps::drillDiameter(),
"board_editor/add_pad/drill_diameter");
mFsmStateConnections.append(
connect(&state, &BoardEditorState_AddPad::drillDiameterChanged,
&mToolDrill, &LengthEditContext::setValuePositive));
mFsmStateConnections.append(
connect(&mToolDrill, &LengthEditContext::valueChangedPositive, &state,
&BoardEditorState_AddPad::setDrillDiameter));
}
// Radius
auto setRadius = [this](const UnsignedLimitedRatio& radius) {
mToolRatio = radius;
onDerivedUiDataChanged.notify();
};
setRadius(state.getRadius());
mFsmStateConnections.append(connect(
&state, &BoardEditorState_AddPad::radiusChanged, this, setRadius));
mFsmStateConnections.append(connect(this, &Board2dTab::ratioRequested, &state,
&BoardEditorState_AddPad::setRadius));
// Fiducial
mToolFiducial = state.getFunctionIsFiducial();
// Press-fit
if (state.getType() == BoardEditorState_AddPad::PadType::THT) {
auto setFunction = [this](Pad::Function function) {
mToolPressFit = function == Pad::Function::PressFitPad;
onDerivedUiDataChanged.notify();
};
setFunction(state.getFunction());
mFsmStateConnections.append(connect(
&state, &BoardEditorState_AddPad::functionChanged, this, setFunction));
mFsmStateConnections.append(connect(
this, &Board2dTab::pressFitRequested, &state, [&state](bool pressFit) {
state.setFunction(pressFit ? Pad::Function::PressFitPad
: Pad::Function::StandardPad);
}));
}
onDerivedUiDataChanged.notify();
}
void Board2dTab::fsmToolEnter(BoardEditorState_DrawPolygon& state) noexcept {
mTool = ui::EditorTool::Polygon;
// Layers
mToolLayersQt = Layer::sorted(state.getAvailableLayers());
mToolLayers->clear();
for (const Layer* layer : std::as_const(mToolLayersQt)) {
mToolLayers->push_back(q2s(layer->getNameTr()));
}
// Layer
auto setLayer = [this](const Layer& layer) {
mToolLayer = &layer;
onDerivedUiDataChanged.notify();
};
setLayer(state.getLayer());
mFsmStateConnections.append(connect(
&state, &BoardEditorState_DrawPolygon::layerChanged, this, setLayer));
mFsmStateConnections.append(connect(this, &Board2dTab::layerRequested, &state,
&BoardEditorState_DrawPolygon::setLayer));
// Line width
mToolLineWidth.configure(state.getLineWidth(),
LengthEditContext::Steps::generic(),
"board_editor/draw_polygon/line_width");
mFsmStateConnections.append(
connect(&state, &BoardEditorState_DrawPolygon::lineWidthChanged,
&mToolLineWidth, &LengthEditContext::setValueUnsigned));
mFsmStateConnections.append(
connect(&mToolLineWidth, &LengthEditContext::valueChangedUnsigned, &state,
&BoardEditorState_DrawPolygon::setLineWidth));
// Angle
auto setAngle = [this](const Angle& angle) {
mToolAngle = angle;
onDerivedUiDataChanged.notify();
};
setAngle(state.getAngle());
mFsmStateConnections.append(connect(
&state, &BoardEditorState_DrawPolygon::angleChanged, this, setAngle));
mFsmStateConnections.append(connect(this, &Board2dTab::angleRequested, &state,
&BoardEditorState_DrawPolygon::setAngle));
// Filled
auto setFilled = [this](bool filled) {
mToolFilled = filled;
onDerivedUiDataChanged.notify();
};
setFilled(state.getFilled());
mFsmStateConnections.append(connect(
&state, &BoardEditorState_DrawPolygon::filledChanged, this, setFilled));
mFsmStateConnections.append(
connect(this, &Board2dTab::filledRequested, &state,
&BoardEditorState_DrawPolygon::setFilled));
onDerivedUiDataChanged.notify();
}
void Board2dTab::fsmToolEnter(BoardEditorState_AddStrokeText& state) noexcept {
mTool = ui::EditorTool::Text;
// Layers
mToolLayersQt = Layer::sorted(state.getAvailableLayers());
mToolLayers->clear();
for (const Layer* layer : std::as_const(mToolLayersQt)) {
mToolLayers->push_back(q2s(layer->getNameTr()));
}
// Layer
auto setLayer = [this](const Layer& layer) {
mToolLayer = &layer;
onDerivedUiDataChanged.notify();
};
setLayer(state.getLayer());
mFsmStateConnections.append(connect(
&state, &BoardEditorState_AddStrokeText::layerChanged, this, setLayer));
mFsmStateConnections.append(
connect(this, &Board2dTab::layerRequested, &state,
&BoardEditorState_AddStrokeText::setLayer));
// Height
mToolSize.configure(state.getHeight(), LengthEditContext::Steps::textHeight(),
"board_editor/add_text/size");
mFsmStateConnections.append(
connect(&state, &BoardEditorState_AddStrokeText::heightChanged,
&mToolSize, &LengthEditContext::setValuePositive));
mFsmStateConnections.append(
connect(&mToolSize, &LengthEditContext::valueChangedPositive, &state,
&BoardEditorState_AddStrokeText::setHeight));
// Text
auto setText = [this](const QString& text) {
mToolValue = text;
onDerivedUiDataChanged.notify();
};
setText(state.getText());
mFsmStateConnections.append(connect(
&state, &BoardEditorState_AddStrokeText::textChanged, this, setText));
mFsmStateConnections.append(
connect(this, &Board2dTab::valueRequested, &state,
&BoardEditorState_AddStrokeText::setText));
// Text suggestions
mToolValueSuggestions->clear();
for (const QString& v : state.getTextSuggestions()) {
mToolValueSuggestions->push_back(q2s(v));
}
// Mirrored
auto setMirrored = [this](bool mirrored) {
mToolMirrored = mirrored;
onDerivedUiDataChanged.notify();
};
setMirrored(state.getMirrored());
mFsmStateConnections.append(
connect(&state, &BoardEditorState_AddStrokeText::mirroredChanged, this,
setMirrored));
mFsmStateConnections.append(
connect(this, &Board2dTab::mirroredRequested, &state,
&BoardEditorState_AddStrokeText::setMirrored));
onDerivedUiDataChanged.notify();
}
void Board2dTab::fsmToolEnter(BoardEditorState_DrawPlane& state) noexcept {
mTool = ui::EditorTool::Plane;
// Nets
mToolNetsQt.clear();
mToolNets->clear();
mToolNetsQt.append(std::make_pair(false, std::nullopt));
mToolNets->push_back(q2s("[" % tr("None") % "]"));
for (const auto& item : state.getAvailableNets()) {
mToolNetsQt.append(std::make_pair(false, item.first));
mToolNets->push_back(q2s(item.second));
}
// Net
auto setNet = [this](const std::optional<Uuid>& net) {
mToolNet = std::make_pair(false, net);
onDerivedUiDataChanged.notify();
};
setNet(state.getNet());
mFsmStateConnections.append(
connect(&state, &BoardEditorState_DrawPlane::netChanged, this, setNet));
mFsmStateConnections.append(
connect(this, &Board2dTab::netRequested, &state,
[&state](bool autoNet, const std::optional<Uuid>& net) {
Q_UNUSED(autoNet);
state.setNet(net);
}));
// Layers
mToolLayersQt = Layer::sorted(state.getAvailableLayers());
mToolLayers->clear();
for (const Layer* layer : std::as_const(mToolLayersQt)) {
mToolLayers->push_back(q2s(layer->getNameTr()));
}
// Layer
auto setLayer = [this](const Layer& layer) {
mToolLayer = &layer;
onDerivedUiDataChanged.notify();
};
setLayer(state.getLayer());
mFsmStateConnections.append(connect(
&state, &BoardEditorState_DrawPlane::layerChanged, this, setLayer));
mFsmStateConnections.append(connect(this, &Board2dTab::layerRequested, &state,
&BoardEditorState_DrawPlane::setLayer));
// Auto-Add
mFsmStateConnections.append(connect(this, &Board2dTab::autoAddPlaneRequested,
&state,
&BoardEditorState_DrawPlane::autoAdd));
onDerivedUiDataChanged.notify();
}
void Board2dTab::fsmToolEnter(BoardEditorState_DrawZone& state) noexcept {
mTool = ui::EditorTool::Zone;
// Available layers
mToolLayersQt = Layer::sorted(state.getAvailableLayers());
mToolLayers->clear();
for (const Layer* layer : std::as_const(mToolLayersQt)) {
mToolLayers->push_back(q2s(layer->getNameTr()));
}
// Layers
auto setLayers = [this](const QSet<const Layer*>& layers) {
if (!layers.isEmpty()) {
mToolLayer = *layers.cbegin(); // ?!?!
}
onDerivedUiDataChanged.notify();
};
setLayers(state.getLayers());
mFsmStateConnections.append(connect(
&state, &BoardEditorState_DrawZone::layersChanged, this, setLayers));
mFsmStateConnections.append(
connect(this, &Board2dTab::layerRequested, &state,
[&state](const Layer& layer) { state.setLayers({&layer}); }));
// Rules
auto setRules = [this](Zone::Rules rules) {
mToolZoneRules = rules;
onDerivedUiDataChanged.notify();
};
setRules(state.getRules());
mFsmStateConnections.append(connect(
&state, &BoardEditorState_DrawZone::rulesChanged, this, setRules));
mFsmStateConnections.append(connect(this, &Board2dTab::zoneRuleRequested,
&state,
&BoardEditorState_DrawZone::setRule));
// Angle
auto setAngle = [this](const Angle& angle) {
mToolAngle = angle;
onDerivedUiDataChanged.notify();
};
setAngle(state.getAngle());
mFsmStateConnections.append(connect(
&state, &BoardEditorState_DrawZone::angleChanged, this, setAngle));
mFsmStateConnections.append(connect(this, &Board2dTab::angleRequested, &state,
&BoardEditorState_DrawZone::setAngle));
onDerivedUiDataChanged.notify();
}
void Board2dTab::fsmToolEnter(BoardEditorState_AddHole& state) noexcept {
mTool = ui::EditorTool::Hole;
// Drill
mToolDrill.configure(state.getDiameter(),
LengthEditContext::Steps::drillDiameter(),
"board_editor/add_hole/diameter");
mFsmStateConnections.append(
connect(&state, &BoardEditorState_AddHole::diameterChanged, &mToolDrill,
&LengthEditContext::setValuePositive));
mFsmStateConnections.append(
connect(&mToolDrill, &LengthEditContext::valueChangedPositive, &state,
&BoardEditorState_AddHole::setDiameter));
onDerivedUiDataChanged.notify();
}
void Board2dTab::fsmToolEnter(BoardEditorState_AddDevice& state) noexcept {
Q_UNUSED(state);
mTool = ui::EditorTool::Component;
onDerivedUiDataChanged.notify();
}
void Board2dTab::fsmToolEnter(BoardEditorState_Measure& state) noexcept {
Q_UNUSED(state);
mTool = ui::EditorTool::Measure;
onDerivedUiDataChanged.notify();
}
/*******************************************************************************
* Private Methods
******************************************************************************/
void Board2dTab::updateEnabledCopperLayers() noexcept {
foreach (const Layer* layer, Layer::innerCopper()) {
if (std::shared_ptr<GraphicsLayer> gLayer = mLayers->get(*layer)) {
gLayer->setEnabled(mBoard.getCopperLayers().contains(layer));
}
}
}
void Board2dTab::loadLayersVisibility() noexcept {
foreach (std::shared_ptr<GraphicsLayer> layer, mLayers->all()) {
if (mBoard.getLayersVisibility().contains(layer->getRole().getId())) {
layer->setVisible(
mBoard.getLayersVisibility().value(layer->getRole().getId()));
}
}
}
void Board2dTab::storeLayersVisibility() noexcept {
QMap<QString, bool> visibility;
foreach (std::shared_ptr<GraphicsLayer> layer, mLayers->all()) {
if (layer->isEnabled()) {
visibility[layer->getRole().getId()] = layer->isVisible();
}
}
mBoard.setLayersVisibility(visibility);
}
void Board2dTab::updateMessages() noexcept {
bool emptySchematics = true;
for (auto cmp : mProject.getCircuit().getComponentInstances()) {
if (!cmp->getLibComponent().isSchematicOnly()) {
emptySchematics = false;
break;
}
}
mMsgEmptySchematics.setActive(emptySchematics);
mMsgSetupDesignRules.setActive(
mBoard.getDeviceInstances().isEmpty() &&
(mBoard.getDesignRules() == BoardDesignRules()) &&
(mBoard.getDrcSettings() == BoardDesignRuleCheckSettings()));
mMsgPlaceDevices.setActive((!emptySchematics) &&
mBoard.getDeviceInstances().isEmpty());
}
void Board2dTab::loadDesignRules(const QString& uiKey) noexcept {
try {
// Load DRC settings.
BoardDesignRuleCheckSettings drcSettings;
if (uiKey != "default") {
const Uuid orgUuid = Uuid::fromString(uiKey.split(":").first());
const Uuid druUuid = Uuid::fromString(uiKey.split(":").last());
std::shared_ptr<const Organization> org =
mApp.getLibraryElementCache().getOrganization(orgUuid,
true); // can throw
const OrganizationPcbDesignRules* dru = org->findPcbDesignRules(druUuid);
if (!dru) {
// Maybe the wrong organization was loaded since the listed organization
// may be from a local library, but the loaded organization from remote?
throw LogicError(__FILE__, __LINE__);
}
drcSettings = dru->getDrcSettings(true);
drcSettings.setSources({BoardDesignRuleCheckSettings::Source{
org->getUuid(),
org->getNames().getDefaultValue(),
org->getVersion(),
dru->getUuid(),
dru->getNames().getDefaultValue(),
}});
}
// Update design rules to conform with the DRC settings.
BoardDesignRules designRules = mBoard.getDesignRules();
designRules.adjustToDrcSettings(drcSettings);
// Apply changes to board.
std::unique_ptr<CmdBoardEdit> cmd = std::make_unique<CmdBoardEdit>(mBoard);
cmd->setDesignRules(designRules);
cmd->setDrcSettings(drcSettings);
mProjectEditor.getUndoStack().execCmd(cmd.release());
// Make sure to hide the "setup design rules" message in the UI.
mMsgSetupDesignRules.dismiss();
} catch (const Exception& e) {
QMessageBox::critical(getWindow(), "Error", e.getMsg());
}
}
void Board2dTab::highlightDrcMessage(
const std::shared_ptr<const RuleCheckMessage>& msg, bool zoomTo) noexcept {
if ((!msg) || msg->getLocations().isEmpty()) {
// Position on board not known.
clearDrcMarker();
} else if (mScene) {
const auto colors = mApp.getWorkspace()
.getSettings()
.boardColorSchemes.getActive()
.getColors(ColorRole::boardOverlays());
QPainterPath path = Path::toQPainterPathPx(msg->getLocations(), true);
mDrcLocationGraphicsItem = std::make_unique<QGraphicsPathItem>();
mDrcLocationGraphicsItem->setZValue(BoardGraphicsScene::ZValue_AirWires);
mDrcLocationGraphicsItem->setPen(QPen(colors.primary, 0));
mDrcLocationGraphicsItem->setBrush(colors.secondary);
mDrcLocationGraphicsItem->setPath(path);
mScene->addItem(*mDrcLocationGraphicsItem.get());
qreal margin = Length(1000000).toPx();
QRectF rect = path.boundingRect();
rect.adjust(-margin, -margin, margin, margin);
mScene->setSceneRectMarker(rect);
if (zoomTo) {
mView->zoomToSceneRect(rect, false);
}
}
}
void Board2dTab::clearDrcMarker() noexcept {
mDrcLocationGraphicsItem.reset();
if (mScene) {
mScene->setSceneRectMarker(QRectF());
}
}
void Board2dTab::scheduleUnplacedComponentsUpdate() noexcept {
if (mUnplacedComponentsUpdateTimer) {
mUnplacedComponentsUpdateTimer->start(100);
}
}
void Board2dTab::updateUnplacedComponents() noexcept {
if (!mUnplacedComponentsModel) return;
mUnplacedComponents.clear();
mUnplacedComponentsModel->clear();
QList<ComponentInstance*> components =
mProject.getCircuit().getComponentInstances().values();
const QMap<Uuid, BI_Device*> boardDeviceList = mBoard.getDeviceInstances();
// Sort components manually using numeric sort.
Toolbox::sortNumeric(
components,
[](const QCollator& cmp, const ComponentInstance* lhs,
const ComponentInstance* rhs) {
return cmp(*lhs->getName(), *rhs->getName());
},
Qt::CaseInsensitive, false);
foreach (const ComponentInstance* cmp, components) {
if (boardDeviceList.contains(cmp->getUuid())) continue;
if (cmp->getLibComponent().isSchematicOnly()) continue;
// Add component to list.
ProjectAttributeLookup lookup(*cmp, nullptr,
cmp->getParts(std::nullopt).value(0));
const QString value =
AttributeSubstitutor::substitute(lookup("VALUE"), lookup)
.split("\n", Qt::SkipEmptyParts)
.join("|");
const QString libCmpName =
*cmp->getLibComponent().getNames().value(mProject.getLocaleOrder());
const QString text =
QString("%1: %2 %3").arg(*cmp->getName(), value, libCmpName);
mUnplacedComponents.append(cmp->getUuid());
mUnplacedComponentsModel->push_back(q2s(text));
}
if (mUnplacedComponents.count() > 0) {
setSelectedUnplacedComponent(
qBound(0, mUnplacedComponentIndex, mUnplacedComponents.count() - 1));
} else {
setSelectedUnplacedComponent(-1);
}
}
void Board2dTab::restartIdleTimer() noexcept {
if (mInputIdleTimer) {
mInputIdleTimer->start();
}
}
void Board2dTab::setSelectedUnplacedComponent(int index) noexcept {
mUnplacedComponentIndex = index;
if ((index >= 0) && (index < mUnplacedComponents.count())) {
mUnplacedComponent = mProject.getCircuit().getComponentInstanceByUuid(
mUnplacedComponents.at(index));
} else {
mUnplacedComponent = nullptr;
}
mUnplacedComponentDevices.clear();
if (mUnplacedComponentDevicesModel) {
mUnplacedComponentDevicesModel->clear();
}
if (mUnplacedComponent && mUnplacedComponentDevicesModel) {
std::pair<QList<DeviceMetadata>, int> devices =
getAvailableDevices(*mUnplacedComponent);
mUnplacedComponentDevices = devices.first;
for (int i = 0; i < mUnplacedComponentDevices.count(); ++i) {
const DeviceMetadata& device = mUnplacedComponentDevices.at(i);
QString text = device.deviceName;
if (!text.contains(device.packageName, Qt::CaseInsensitive)) {
// Package name not contained in device name, so let's show it as well.
text += " [" % device.packageName % "]";
}
if (device.isListedInComponentInstance) {
text += " ✔";
}
mUnplacedComponentDevicesModel->push_back(q2s(text));
}
setSelectedUnplacedComponentDevice(devices.second);
} else {
setSelectedUnplacedComponentDevice(-1);
}
}
void Board2dTab::setSelectedUnplacedComponentDevice(int index) noexcept {
mUnplacedComponentDeviceIndex = index;
// Abort if index is out of bounds.
if ((index < 0) || (index >= mUnplacedComponentDevices.count())) {
setSelectedUnplacedComponentDeviceAndPackage(std::nullopt, nullptr, false);
return;
}
try {
const DeviceMetadata& device = mUnplacedComponentDevices.at(index);
bool packageOwned = false;
// Prefer package in project library for several reasons:
// - Allow adding devices even if package not found in workspace library
// - Use correct package (version) for preview
// - Better performance than loading workspace library elements
Package* pkg = mProject.getLibrary().getPackage(device.packageUuid);
if (!pkg) {
// If package does not exist in project library, use workspace library.
const FilePath pkgFp =
mApp.getWorkspace().getLibraryDb().getLatest<Package>(
device.packageUuid);
if (pkgFp.isValid()) {
pkg = Package::open(std::make_unique<TransactionalDirectory>(
TransactionalFileSystem::openRO(pkgFp)))
.release();
packageOwned = true;
}
}
setSelectedUnplacedComponentDeviceAndPackage(device.deviceUuid, pkg,
packageOwned);
} catch (const Exception& e) {
qCritical() << "Failed to load device & package preview:" << e.getMsg();
setSelectedUnplacedComponentDeviceAndPackage(std::nullopt, nullptr, false);
}
}
void Board2dTab::setSelectedUnplacedComponentDeviceAndPackage(
const std::optional<Uuid>& deviceUuid, Package* package,
bool packageOwned) noexcept {
if (mUnplacedComponentFootprintsModel) {
mUnplacedComponentFootprintsModel->clear();
}
if (mUnplacedComponentPackageOwned) {
delete mUnplacedComponentPackage;
}
mUnplacedComponentPackage = nullptr;
mUnplacedComponentPackageOwned = false;
int fptIndex = 0;
if (deviceUuid && package && mUnplacedComponentFootprintsModel) {
mUnplacedComponentPackage = package;
mUnplacedComponentPackageOwned = packageOwned;
for (const Footprint& fpt : mUnplacedComponentPackage->getFootprints()) {
mUnplacedComponentFootprintsModel->push_back(
q2s(*fpt.getNames().value(mProject.getLocaleOrder())));
}
// Select most relevant footprint.
if (auto uuid = getSuggestedFootprint(mUnplacedComponentPackage->getUuid(),
package)) {
fptIndex = std::max(package->getFootprints().indexOf(*uuid), 0);
}
}
setSelectedUnplacedComponentFootprint(fptIndex);
}
void Board2dTab::setSelectedUnplacedComponentFootprint(int index) noexcept {
mUnplacedComponentFootprintIndex = index;
if (mUnplacedComponentGraphicsScene && mUnplacedComponentGraphicsItem) {
mUnplacedComponentGraphicsScene->removeItem(
*mUnplacedComponentGraphicsItem);
mUnplacedComponentGraphicsItem.reset();
}
if (mUnplacedComponent && mUnplacedComponentPackage) {
if (auto fpt = mUnplacedComponentPackage->getFootprints().value(index)) {
mUnplacedComponentGraphicsItem = std::make_unique<FootprintGraphicsItem>(
fpt, mApp.getPreviewLayers(), Application::getDefaultStrokeFont(),
&mUnplacedComponentPackage->getPads(),
&mUnplacedComponent->getLibComponent(), mProject.getLocaleOrder());
mUnplacedComponentGraphicsScene->addItem(*mUnplacedComponentGraphicsItem);
}
}
requestRepaint();
}
std::pair<QList<Board2dTab::DeviceMetadata>, int>
Board2dTab::getAvailableDevices(ComponentInstance& cmp) const noexcept {
QList<DeviceMetadata> devices;
Uuid cmpUuid = cmp.getLibComponent().getUuid();
QStringList localeOrder = mProject.getLocaleOrder();
// Get matching devices in project library.
QHash<Uuid, Device*> prjLibDev =
mProject.getLibrary().getDevicesOfComponent(cmpUuid);
for (auto i = prjLibDev.constBegin(); i != prjLibDev.constEnd(); ++i) {
devices.append(
DeviceMetadata{i.key(), *i.value()->getNames().value(localeOrder),
i.value()->getPackageUuid(), QString(), false});
}
// Get matching devices in workspace library.
try {
QSet<Uuid> wsLibDev =
mApp.getWorkspace().getLibraryDb().getComponentDevices(
cmpUuid); // can throw
wsLibDev -= Toolbox::toSet(prjLibDev.keys());
foreach (const Uuid& deviceUuid, wsLibDev) {
// Get device metadata.
FilePath devFp = mApp.getWorkspace().getLibraryDb().getLatest<Device>(
deviceUuid); // can throw
if (!devFp.isValid()) continue;
QString devName;
mApp.getWorkspace().getLibraryDb().getTranslations<Device>(
devFp, localeOrder,
&devName); // can throw
Uuid pkgUuid = Uuid::createRandom(); // Temporary.
mApp.getWorkspace().getLibraryDb().getDeviceMetadata(
devFp, nullptr,
&pkgUuid); // can throw
devices.append(
DeviceMetadata{deviceUuid, devName, pkgUuid, QString(), false});
}
} catch (const Exception& e) {
qCritical() << "Failed to list devices in unplaced components dock:"
<< e.getMsg();
}
// Determine missing metadata.
const QSet<Uuid> cmpDevices = cmp.getCompatibleDevices();
for (DeviceMetadata& device : devices) {
device.isListedInComponentInstance = cmpDevices.contains(device.deviceUuid);
if (const Package* package =
mProject.getLibrary().getPackage(device.packageUuid)) {
device.packageName = *package->getNames().value(localeOrder);
} else {
try {
FilePath pkgFp = mApp.getWorkspace().getLibraryDb().getLatest<Package>(
device.packageUuid); // can throw
if (!pkgFp.isValid()) continue;
mApp.getWorkspace().getLibraryDb().getTranslations<Package>(
pkgFp, localeOrder,
&device.packageName); // can throw
} catch (const Exception& e) {
qCritical() << "Failed to query packages in unplaced components dock:"
<< e.getMsg();
}
}
}
// Sort by device name, using numeric sort.
Toolbox::sortNumeric(
devices,
[](const QCollator& cmp, const DeviceMetadata& lhs,
const DeviceMetadata& rhs) {
return cmp(lhs.deviceName, rhs.deviceName);
},
Qt::CaseInsensitive, false);
// Prio 1: Use the device already used for the same component before, if it
// is chosen in the component instance.
auto lastDeviceIterator = mLastDeviceOfComponent.find(cmpUuid);
if ((lastDeviceIterator != mLastDeviceOfComponent.end())) {
for (int i = 0; i < devices.count(); ++i) {
if (devices.at(i).isListedInComponentInstance &&
(devices.at(i).deviceUuid == *lastDeviceIterator)) {
return std::make_pair(devices, i);
}
}
}
// Prio 2: Use the first device chosen in the component instance.
for (int i = 0; i < devices.count(); ++i) {
if (devices.at(i).isListedInComponentInstance) {
return std::make_pair(devices, i);
}
}
// Prio 3: Use the device already used for the same component before.
if ((lastDeviceIterator != mLastDeviceOfComponent.end())) {
for (int i = 0; i < devices.count(); ++i) {
if (devices.at(i).deviceUuid == *lastDeviceIterator) {
return std::make_pair(devices, i);
}
}
}
// Prio 4: Use the most used device in the current board.
QHash<Uuid, int> devOccurrences;
foreach (const BI_Device* device, mBoard.getDeviceInstances()) {
Q_ASSERT(device);
if (device->getComponentInstance().getLibComponent().getUuid() ==
cmp.getLibComponent().getUuid()) {
++devOccurrences[device->getLibDevice().getUuid()];
}
}
auto maxCountIt =
std::max_element(devOccurrences.constBegin(), devOccurrences.constEnd());
if (maxCountIt != devOccurrences.constEnd()) {
for (int i = 0; i < devices.count(); ++i) {
if (devOccurrences.value(devices.at(i).deviceUuid) == (*maxCountIt)) {
return std::make_pair(devices, i);
}
}
}
// Prio 5: Use the first device found in the project library.
for (int i = 0; i < devices.count(); ++i) {
if (prjLibDev.contains(devices.at(i).deviceUuid)) {
return std::make_pair(devices, i);
}
}
// Prio 6: Use the first device found in the workspace library.
return std::make_pair(devices, devices.isEmpty() ? -1 : 0);
}
std::optional<Uuid> Board2dTab::getSuggestedFootprint(
const Uuid& libPkgUuid, const Package* pkg) const noexcept {
// Prio 1: Use the footprint already used for the same device before.
auto lastFootprintIterator = mLastFootprintOfPackage.find(libPkgUuid);
if ((lastFootprintIterator != mLastFootprintOfPackage.end())) {
return *lastFootprintIterator;
}
// Prio 2: Use the most used footprint in the current board.
QHash<Uuid, int> fptOccurrences;
foreach (const BI_Device* device, mBoard.getDeviceInstances()) {
if (device->getLibPackage().getUuid() == libPkgUuid) {
++fptOccurrences[device->getLibFootprint().getUuid()];
}
}
auto maxCountIt =
std::max_element(fptOccurrences.constBegin(), fptOccurrences.constEnd());
if (maxCountIt != fptOccurrences.constEnd()) {
QList<Uuid> uuids = fptOccurrences.keys(*maxCountIt);
if (uuids.count() > 0) {
return uuids.first();
}
}
// Prio 3: Use the footprint which matches the preferred tags of the
// current board. Unfortunately this is only possible in the manual mode
// because in that case we have access to the whole package, not just the
// library database.
if (pkg && (!pkg->getFootprints().isEmpty())) {
const Board::PreferredFootprintTags& boardTags =
mBoard.getPreferredFootprintTags();
const Package::AssemblyType at = pkg->getAssemblyType(true);
const QVector<Tag> tags = ((at == Package::AssemblyType::Tht) ||
(at == Package::AssemblyType::Mixed))
? (boardTags.thtTop + boardTags.common)
: (boardTags.smtTop + boardTags.common);
TagMatcher matcher;
for (const Footprint& fpt : pkg->getFootprints()) {
matcher.addOption(fpt.getTags());
}
if (auto fpt = pkg->getFootprints().value(matcher.findFirstMatch(tags))) {
return fpt->getUuid();
}
}
// Prio 4: Fallback to the default footprint.
return std::nullopt;
}
void Board2dTab::addUnplacedComponentsToBoard(
PlaceComponentsMode mode) noexcept {
// Parse & validate state.
if ((!mUnplacedComponent) || (!mUnplacedComponentPackage)) {
return;
}
if ((mUnplacedComponentDeviceIndex < 0) ||
(mUnplacedComponentDeviceIndex >= mUnplacedComponentDevices.count())) {
return;
}
const DeviceMetadata& selectedDevice =
mUnplacedComponentDevices.at(mUnplacedComponentDeviceIndex);
std::shared_ptr<const Footprint> selectedFootprint =
mUnplacedComponentPackage->getFootprints().value(
mUnplacedComponentFootprintIndex);
if (!selectedFootprint) {
return;
}
// Release undo stack.
emit mProjectEditor.abortBlockingToolsInOtherEditors(this);
// Memorize selection.
if (mode != PlaceComponentsMode::All) {
mLastDeviceOfComponent.insert(
mUnplacedComponent->getLibComponent().getUuid(),
selectedDevice.deviceUuid);
mLastFootprintOfPackage.insert(mUnplacedComponentPackage->getUuid(),
selectedFootprint->getUuid());
}
// Single mode is interactive and handled by FSM.
if (mode == PlaceComponentsMode::Single) {
mFsm->processAddDevice(*mUnplacedComponent, selectedDevice.deviceUuid,
selectedFootprint->getUuid());
return;
}
// Multi-mode is handled here.
Point nextPos = Point::fromMm(0, -20);
if (mScene) {
nextPos += Point::fromPx(mScene->itemsBoundingRect().bottomLeft());
}
std::unique_ptr<UndoCommandGroup> cmd(
new UndoCommandGroup(tr("Add devices to board")));
for (const Uuid& cmpUuid : std::as_const(mUnplacedComponents)) {
ComponentInstance* cmp =
mProject.getCircuit().getComponentInstanceByUuid(cmpUuid);
if (cmp &&
((mode == PlaceComponentsMode::All) ||
(cmp->getLibComponent().getUuid() ==
mUnplacedComponent->getLibComponent().getUuid()))) {
const std::pair<QList<DeviceMetadata>, int> devices =
getAvailableDevices(*cmp);
if ((devices.second >= 0) && (devices.second < devices.first.count())) {
const DeviceMetadata& dev = devices.first.at(devices.second);
const std::optional<Uuid> fptUuid =
getSuggestedFootprint(dev.packageUuid, nullptr);
cmd->appendChild(new CmdAddDeviceToBoard(
mApp.getWorkspace(), mBoard, *cmp, dev.deviceUuid, fptUuid,
std::nullopt, nextPos.mapToGrid(mBoard.getGridInterval())));
if (nextPos.getX() > Length::fromMm(100)) {
nextPos = Point::fromMm(0, nextPos.getY().toMm() - 10);
} else {
nextPos += Point::fromMm(10, 0);
}
}
}
}
try {
mProjectEditor.getUndoStack().execCmd(cmd.release());
} catch (const Exception& e) {
QMessageBox::critical(getWindow(), tr("Error"), e.getMsg());
}
}
void Board2dTab::execGraphicsExportDialog(GraphicsExportDialog::Output output,
const QString& settingsKey) noexcept {
try {
// Determine default file path.
QString projectName = FilePath::cleanFileName(
*mProject.getName(), FilePath::ReplaceSpaces | FilePath::KeepCase);
QString projectVersion = FilePath::cleanFileName(
*mProject.getVersion(), FilePath::ReplaceSpaces | FilePath::KeepCase);
QString relativePath =
QString("output/%1/%2_Board").arg(projectVersion, projectName);
FilePath defaultFilePath = mProject.getPath().getPathTo(relativePath);
// Copy board to allow processing it in worker threads.
// Also rebuild outdated planes to avoid exporting wrong data.
QProgressDialog progress(tr("Preparing board..."), tr("Cancel"), 0, 1,
getWindow());
progress.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(100);
const auto layers = mBoard.getCopperLayers();
BoardPlaneFragmentsBuilder builder;
connect(&progress, &QProgressDialog::canceled, &builder,
&BoardPlaneFragmentsBuilder::cancel);
if (builder.start(mBoard, &layers)) {
BoardPlaneFragmentsBuilder::Result result = builder.waitForFinished();
result.throwOnError();
result.applyToBoard();
}
QList<std::shared_ptr<GraphicsPagePainter>> pages{
std::make_shared<BoardPainter>(mBoard)};
progress.setValue(1);
if (progress.wasCanceled()) {
return;
}
// Show dialog, which will do all the work.
GraphicsExportDialog dialog(
GraphicsExportDialog::Mode::Board, output, pages, 0,
*mProject.getName(), mBoard.getInnerLayerCount(), defaultFilePath,
mApp.getWorkspace().getSettings().defaultLengthUnit.get(),
mApp.getWorkspace().getSettings().boardColorSchemes.getActive(),
"board_editor/" % settingsKey, getWindow());
connect(&dialog, &GraphicsExportDialog::requestOpenFile, this,
[this](const FilePath& fp) {
DesktopServices ds(mApp.getWorkspace().getSettings());
ds.openLocalPath(fp);
});
dialog.exec();
} catch (const Exception& e) {
QMessageBox::warning(getWindow(), tr("Error"), e.getMsg());
}
}
void Board2dTab::execPickPlaceExportDialog() noexcept {
BoardPickPlaceGeneratorDialog dialog(mApp.getWorkspace().getSettings(),
mBoard);
dialog.exec();
}
void Board2dTab::execD356NetlistExportDialog() noexcept {
try {
QString path = "output/{{VERSION}}/{{PROJECT}}_Netlist.d356";
path = AttributeSubstitutor::substitute(
path, ProjectAttributeLookup(mBoard, nullptr), [&](const QString& str) {
return FilePath::cleanFileName(
str, FilePath::ReplaceSpaces | FilePath::KeepCase);
});
path = FileDialog::getSaveFileName(
getWindow(), tr("Export IPC D-356A Netlist"),
mProject.getPath().getPathTo(path).toStr(), "*.d356");
if (path.isEmpty()) return;
if (!path.contains(".")) path.append(".d356");
FilePath fp(path);
qDebug().nospace() << "Export IPC D-356A netlist to " << fp.toNative()
<< "...";
BoardD356NetlistExport exp(mBoard);
FileUtils::writeFile(fp, exp.generate()); // can throw
qDebug() << "Successfully exported netlist.";
} catch (const Exception& e) {
QMessageBox::critical(getWindow(), tr("Error"), e.getMsg());
}
}
void Board2dTab::execSpecctraExportDialog() noexcept {
try {
// Default file path.
QString path = "output/{{VERSION}}/{{PROJECT}}";
if (mProject.getBoards().count() > 1) {
path += "_{{BOARD}}";
}
path += ".dsn";
path = AttributeSubstitutor::substitute(
path, ProjectAttributeLookup(mBoard, nullptr), [&](const QString& str) {
return FilePath::cleanFileName(
str, FilePath::ReplaceSpaces | FilePath::KeepCase);
});
// Use memorized file path, if board path and version number match.
QSettings cs;
const QString csId =
mBoard.getDirectory().getAbsPath().toStr() + *mProject.getVersion();
const QString csKey = "board_editor/dsn_export/" %
QString(QCryptographicHash::hash(csId.toUtf8(), QCryptographicHash::Md5)
.toHex());
path = cs.value(csKey, path).toString();
// Make file path absolute.
if (QFileInfo(path).isRelative()) {
path = mProject.getPath().getPathTo(path).toStr();
}
// Choose file path.
path = FileDialog::getSaveFileName(
getWindow(),
EditorCommandSet::instance().exportSpecctraDsn.getDisplayText(), path,
"*.dsn");
if (path.isEmpty()) return;
if (!path.contains(".")) path.append(".dsn");
const FilePath fp(path);
// Memorize file path.
cs.setValue(csKey,
fp.isLocatedInDir(mProject.getPath())
? fp.toRelative(mProject.getPath())
: fp.toNative());
// Perform export.
qDebug().nospace() << "Export Specctra DSN to " << fp.toNative() << "...";
BoardSpecctraExport exp(mBoard);
FileUtils::writeFile(fp, exp.generate()); // can throw
qDebug() << "Successfully exported Specctra DSN.";
emit statusBarMessageChanged(tr("Success!"), 3000);
} catch (const Exception& e) {
QMessageBox::critical(getWindow(), tr("Error"), e.getMsg());
}
}
void Board2dTab::execSpecctraImportDialog() noexcept {
auto logger = std::make_shared<MessageLogger>();
logger->warning(
tr("This is a new feature and we could test it only with very few "
"external routers. If you experience any compatibility issue with "
"your router, please let us know!"));
logger->warning(" → https://librepcb.org/help/");
try {
// Use memorized export file path, if board path and version number match.
QSettings cs;
const QString csId =
mBoard.getDirectory().getAbsPath().toStr() + *mProject.getVersion();
const QString csKey = "board_editor/dsn_export/" %
QString(QCryptographicHash::hash(csId.toUtf8(), QCryptographicHash::Md5)
.toHex());
QString path = cs.value(csKey).toString().replace(".dsn", ".ses");
// Make file path absolute.
if (QFileInfo(path).isRelative()) {
path = mProject.getPath().getPathTo(path).toStr();
}
// Choose file path.
path = FileDialog::getOpenFileName(
getWindow(),
EditorCommandSet::instance().importSpecctraSes.getDisplayText(), path,
"*.ses;;*");
if (path.isEmpty()) return;
const FilePath fp(path);
// Release undo stack.
emit mProjectEditor.abortBlockingToolsInOtherEditors(this);
// Set UI into busy state during the import.
QApplication::setOverrideCursor(Qt::WaitCursor);
auto cursorScopeGuard =
scopeGuard([]() { QApplication::restoreOverrideCursor(); });
// Perform import.
qDebug().nospace() << "Import Specctra SES from " << fp.toNative() << "...";
logger->debug(tr("Parsing Specctra session '%1'...").arg(fp.toNative()));
const QByteArray content = FileUtils::readFile(fp); // can throw
std::unique_ptr<SExpression> root =
SExpression::parse(content, fp, SExpression::Mode::Permissive);
mProjectEditor.getUndoStack().execCmd(
new CmdBoardSpecctraImport(mBoard, *root, logger)); // can throw
qDebug() << "Successfully imported Specctra SES.";
} catch (const Exception& e) {
logger->critical(e.getMsg());
logger->critical(tr("Import failed, no changes made to the board."));
}
// Display messages.
QDialog dlg(getWindow());
dlg.setWindowTitle(tr("Specctra SES Import"));
dlg.setMinimumSize(600, 400);
QVBoxLayout* layout = new QVBoxLayout(&dlg);
QTextBrowser* txtBrowser = new QTextBrowser(&dlg);
txtBrowser->setReadOnly(true);
txtBrowser->setWordWrapMode(QTextOption::WordWrap);
txtBrowser->setText(logger->getMessagesRichText(&mApp.getTheme()));
txtBrowser->verticalScrollBar()->setValue(
txtBrowser->verticalScrollBar()->maximum());
layout->addWidget(txtBrowser);
QPushButton* btnClose = new QPushButton(tr("Close"), &dlg);
connect(btnClose, &QPushButton::clicked, &dlg, &QDialog::accept);
layout->addWidget(btnClose);
dlg.exec();
}
void Board2dTab::goToObjects(
const QVector<SearchContext::Candidate>& objects) noexcept {
if (!mScene) return;
// Cross-probe found objects.
QSet<const NetSignal*> nets;
QSet<const ComponentInstance*> components;
for (const auto& obj : objects) {
if (obj.type == SearchContext::ObjectType::Net) {
if (auto net = mProject.getCircuit().getNetSignalByName(obj.name)) {
nets.insert(net);
}
} else if (obj.type == SearchContext::ObjectType::Component) {
if (auto cmp =
mProject.getCircuit().getComponentInstanceByName(obj.name)) {
components.insert(cmp);
}
}
}
mProjectEditor.getCrossProbe()->set(nullptr, nets, components, {}, {});
// Select objects & zoom to them.
mScene->clearSelection();
QRectF bbox;
auto setSelected = [&bbox](QGraphicsItem* item) {
item->setSelected(true);
bbox |= item->mapRectToScene(item->boundingRect() |
item->childrenBoundingRect());
};
if (!components.isEmpty()) {
for (const auto& item : mScene->getDevices()) {
if (components.contains(&item->getDevice().getComponentInstance())) {
setSelected(item.get());
}
}
}
if (!nets.isEmpty()) {
for (const auto& item : mScene->getPads()) {
const NetSignal* net = item->getPad().getNetSignal();
if (net && nets.contains(net)) {
setSelected(item.get());
}
}
for (const auto& item : mScene->getVias()) {
const NetSignal* net = item->getVia().getNetSegment().getNetSignal();
if (net && nets.contains(net)) {
setSelected(item.get());
}
}
for (const auto& item : mScene->getNetLines()) {
const NetSignal* net = item->getNetLine().getNetSegment().getNetSignal();
if (net && nets.contains(net)) {
setSelected(item.get());
}
}
for (const auto& item : mScene->getPlanes()) {
const NetSignal* net = item->getPlane().getNetSignal();
if (net && nets.contains(net)) {
setSelected(item.get());
}
}
}
if (!bbox.isEmpty()) {
// Zoom to a rectangle relative to the maximum graphics item dimension,
// occupying 1/4th of the screen, but limiting the margin to 10mm.
const qreal margin =
std::min(1.5f * std::max(bbox.size().width(), bbox.size().height()),
Length::fromMm(10).toPx());
bbox.adjust(-margin, -margin, margin, margin);
mView->zoomToSceneRect(bbox, false);
}
// Enforce updating the info box for the selected objects.
mFsm->processChangedSelection();
}
bool Board2dTab::toggleBackgroundImage() noexcept {
if (mBackgroundImageGraphicsItem->isVisible()) {
mBackgroundImageSettings.enabled = false;
} else {
// Show dialog.
BackgroundImageSetupDialog dlg("board_editor", getWindow());
if (!mBackgroundImageSettings.image.isNull()) {
dlg.setData(mBackgroundImageSettings.image,
mBackgroundImageSettings.rotation,
mBackgroundImageSettings.references);
}
if (dlg.exec() != QDialog::Accepted) {
return mBackgroundImageGraphicsItem->isVisible(); // Aborted.
}
mBackgroundImageSettings.image = dlg.getImage();
mBackgroundImageSettings.rotation = dlg.getRotation();
mBackgroundImageSettings.references = dlg.getReferences();
mBackgroundImageSettings.enabled =
(!mBackgroundImageSettings.image.isNull()) &&
(mBackgroundImageSettings.references.count() >= 2);
// Make sure the loaded image is actually visible.
if (mBackgroundImageSettings.enabled &&
(mBackgroundImageGraphicsItem->opacity() < 0.1)) {
mBackgroundImageGraphicsItem->setOpacity(0.8);
}
}
// Store & apply new settings.
mBackgroundImageSettings.saveToDir(getBackgroundImageCacheDir());
applyBackgroundImageSettings();
onDerivedUiDataChanged.notify();
return mBackgroundImageGraphicsItem->isVisible();
}
void Board2dTab::applyBackgroundImageSettings() noexcept {
BackgroundImageSettings& s = mBackgroundImageSettings;
const bool enable = s.enabled && (!s.image.isNull());
mBackgroundImageGraphicsItem->setVisible(enable);
if (enable) {
// Post-process the image.
const ColorScheme& scheme =
mApp.getWorkspace().getSettings().boardColorSchemes.getActive();
mBackgroundImageGraphicsItem->setPixmap(
s.buildPixmap(scheme.getColors(ColorRole::boardBackground()).primary));
// Apply the image & transform.
mBackgroundImageGraphicsItem->setTransform(s.calcTransform());
if (s.references.count() >= 1) {
mBackgroundImageGraphicsItem->setPos(
s.references.first().second.toPxQPointF());
}
}
}
FilePath Board2dTab::getBackgroundImageCacheDir() const noexcept {
return Application::getCacheDir()
.getPathTo("backgrounds")
.getPathTo(mBoard.getUuid().toStr());
}
void Board2dTab::applyWorkspaceSettings() noexcept {
const ColorScheme& scheme =
mApp.getWorkspace().getSettings().boardColorSchemes.getActive();
if (mScene) {
const auto background = scheme.getColors(ColorRole::boardBackground());
mScene->setBackgroundColors(background.primary, background.secondary);
const auto overlay = scheme.getColors(ColorRole::boardOverlays());
mScene->setOverlayColors(overlay.primary, overlay.secondary);
const auto selection = scheme.getColors(ColorRole::boardSelection());
mScene->setSelectionRectColors(selection.primary, selection.secondary);
mScene->setGridStyle(mGridStyle);
}
if (mUnplacedComponentGraphicsScene) {
const auto background = scheme.getColors(ColorRole::boardBackground());
mUnplacedComponentGraphicsScene->setBackgroundColors(background.primary,
background.secondary);
}
onDerivedUiDataChanged.notify();
}
void Board2dTab::requestRepaint() noexcept {
++mFrameIndex;
onDerivedUiDataChanged.notify();
}
/*******************************************************************************
* End of File
******************************************************************************/
} // namespace editor
} // namespace librepcb