diff --git a/common/advanced_config.cpp b/common/advanced_config.cpp index 7d9cd58037..65e47a71c8 100644 --- a/common/advanced_config.cpp +++ b/common/advanced_config.cpp @@ -221,6 +221,8 @@ static const wxChar EnableEeschemaPrintCairo[] = wxT( "EnableEeschemaPrintCairo" * The time in milliseconds to wait before displaying a disambiguation menu. */ static const wxChar DisambiguationTime[] = wxT( "DisambiguationTime" ); + +static const wxChar PcbSelectionVisibilityRatio[] = wxT( "PcbSelectionVisibilityRatio" ); } // namespace KEYS @@ -360,6 +362,8 @@ ADVANCED_CFG::ADVANCED_CFG() m_DisambiguationMenuDelay = 300; + m_PcbSelectionVisibilityRatio = 1.0; + loadFromConfigFile(); } @@ -528,6 +532,10 @@ void ADVANCED_CFG::loadSettings( wxConfigBase& aCfg ) &m_EnableEeschemaPrintCairo, m_EnableEeschemaPrintCairo ) ); + configParams.push_back( new PARAM_CFG_DOUBLE( true, AC_KEYS::PcbSelectionVisibilityRatio, + &m_PcbSelectionVisibilityRatio, + m_PcbSelectionVisibilityRatio, 0.0, 1.0 ) ); + // Special case for trace mask setting...we just grab them and set them immediately // Because we even use wxLogTrace inside of advanced config wxString traceMasks; diff --git a/include/advanced_config.h b/include/advanced_config.h index 07e741b0d2..afd246fb0d 100644 --- a/include/advanced_config.h +++ b/include/advanced_config.h @@ -284,6 +284,18 @@ public: */ bool m_EnableEeschemaPrintCairo; + /** + * Board object selection visibility limit. + * + * This ratio is used to determine if an object in a selected object layer stack is + * visible. All alpha ratios less or equal to this value are considered invisible + * to the user and will be pruned from the list of selections. Valid values are + * between 0 and less than 1. A value of 1 disables this feature. Reasonable values + * are between 0.01 and 0.03 depending on the layer colors. + * + * The setting name is "PcbSelectionVisibilityRatio". + */ + double m_PcbSelectionVisibilityRatio; ///@} diff --git a/include/layer_ids.h b/include/layer_ids.h index 6b50f34f18..95a2114ea8 100644 --- a/include/layer_ids.h +++ b/include/layer_ids.h @@ -540,6 +540,17 @@ public: { return at( m_index ); // throws std::out_of_range } + + int TestLayers( PCB_LAYER_ID aRhs, PCB_LAYER_ID aLhs ) const + { + if( aRhs == aLhs ) + return 0; + + auto itRhs = std::find( begin(), end(), aRhs ); + auto itLhs = std::find( begin(), end(), aLhs ); + + return std::distance( itRhs, itLhs ); + } }; diff --git a/pcbnew/tools/pcb_selection_tool.cpp b/pcbnew/tools/pcb_selection_tool.cpp index f26765988a..e68557e6b4 100644 --- a/pcbnew/tools/pcb_selection_tool.cpp +++ b/pcbnew/tools/pcb_selection_tool.cpp @@ -74,6 +74,14 @@ using namespace std::placeholders; #include +struct LAYER_OPACITY_ITEM +{ + PCB_LAYER_ID m_Layer; + double m_Opacity; + const BOARD_ITEM* m_Item;; +}; + + class SELECT_MENU : public ACTION_MENU { public: @@ -2548,6 +2556,7 @@ void PCB_SELECTION_TOOL::RebuildSelection() bool PCB_SELECTION_TOOL::Selectable( const BOARD_ITEM* aItem, bool checkVisibilityOnly ) const { const RENDER_SETTINGS* settings = getView()->GetPainter()->GetSettings(); + const PCB_DISPLAY_OPTIONS& options = frame()->GetDisplayOptions(); auto visibleLayers = [&]() @@ -2652,7 +2661,7 @@ bool PCB_SELECTION_TOOL::Selectable( const BOARD_ITEM* aItem, bool checkVisibili switch( aItem->Type() ) { case PCB_ZONE_T: - if( !board()->IsElementVisible( LAYER_ZONES ) ) + if( !board()->IsElementVisible( LAYER_ZONES ) || ( options.m_ZoneOpacity == 0.00 ) ) return false; zone = static_cast( aItem ); @@ -2679,7 +2688,7 @@ bool PCB_SELECTION_TOOL::Selectable( const BOARD_ITEM* aItem, bool checkVisibili case PCB_TRACE_T: case PCB_ARC_T: - if( !board()->IsElementVisible( LAYER_TRACKS ) ) + if( !board()->IsElementVisible( LAYER_TRACKS ) || ( options.m_TrackOpacity == 0.00 ) ) return false; if( m_isFootprintEditor ) @@ -2696,7 +2705,7 @@ bool PCB_SELECTION_TOOL::Selectable( const BOARD_ITEM* aItem, bool checkVisibili break; case PCB_VIA_T: - if( !board()->IsElementVisible( LAYER_VIAS ) ) + if( !board()->IsElementVisible( LAYER_VIAS ) || ( options.m_ViaOpacity == 0.00 ) ) return false; via = static_cast( aItem ); @@ -2750,9 +2759,14 @@ bool PCB_SELECTION_TOOL::Selectable( const BOARD_ITEM* aItem, bool checkVisibili break; + case PCB_REFERENCE_IMAGE_T: + if( options.m_ImageOpacity == 0.00 ) + return false; + + KI_FALLTHROUGH; + case PCB_SHAPE_T: case PCB_TEXTBOX_T: - case PCB_REFERENCE_IMAGE_T: if( m_isFootprintEditor ) { if( !view()->IsLayerVisible( aItem->GetLayer() ) ) @@ -2793,6 +2807,9 @@ bool PCB_SELECTION_TOOL::Selectable( const BOARD_ITEM* aItem, bool checkVisibili break; case PCB_PAD_T: + if( options.m_PadOpacity == 0.00 ) + return false; + pad = static_cast( aItem ); if( pad->GetAttribute() == PAD_ATTRIB::PTH || pad->GetAttribute() == PAD_ATTRIB::NPTH ) @@ -3068,6 +3085,153 @@ int PCB_SELECTION_TOOL::hitTestDistance( const VECTOR2I& aWhere, BOARD_ITEM* aIt } +void PCB_SELECTION_TOOL::pruneObscuredSelectionCandidates( GENERAL_COLLECTOR& aCollector ) const +{ + wxCHECK( m_frame, /* void */ ); + + if( aCollector.GetCount() < 2 ) + return; + + const RENDER_SETTINGS* settings = getView()->GetPainter()->GetSettings(); + + wxCHECK( settings, /* void */ ); + + PCB_LAYER_ID activeLayer = m_frame->GetActiveLayer(); + LSET visibleLayers = m_frame->GetBoard()->GetVisibleLayers(); + LSET enabledLayers = m_frame->GetBoard()->GetEnabledLayers(); + LSEQ enabledLayerStack = enabledLayers.SeqStackupTop2Bottom( activeLayer ); + + wxCHECK( !enabledLayerStack.empty(), /* void */ ); + + auto isCopperPourKeepoutZone = []( const BOARD_ITEM* aItem ) -> bool + { + if( aItem->Type() == PCB_ZONE_T ) + { + const ZONE* zone = static_cast( aItem ); + + wxCHECK( zone, false ); + + if( zone->GetIsRuleArea() + && zone->GetDoNotAllowCopperPour() ) + return true; + } + + return false; + }; + + std::vector opacityStackup; + + for( int i = 0; i < aCollector.GetCount(); i++ ) + { + const BOARD_ITEM* item = aCollector[i]; + + LSET itemLayers = item->GetLayerSet() & enabledLayers & visibleLayers; + LSEQ itemLayerSeq = itemLayers.Seq( enabledLayerStack ); + + for( PCB_LAYER_ID layer : itemLayerSeq ) + { + COLOR4D color = settings->GetColor( item, layer ); + + if( color.a == 0 ) + continue; + + LAYER_OPACITY_ITEM opacityItem; + + opacityItem.m_Layer = layer; + opacityItem.m_Opacity = color.a; + opacityItem.m_Item = item; + + if( isCopperPourKeepoutZone( item ) ) + opacityItem.m_Opacity = 0.0; + + opacityStackup.emplace_back( opacityItem ); + } + } + + std::sort( opacityStackup.begin(), opacityStackup.end(), + [&]( const LAYER_OPACITY_ITEM& aLhs, const LAYER_OPACITY_ITEM& aRhs ) -> bool + { + int retv = enabledLayerStack.TestLayers( aLhs.m_Layer, aRhs.m_Layer ); + + if( retv ) + return retv > 0; + + return aLhs.m_Opacity > aRhs.m_Opacity; + } ); + + std::set visibleItems; + std::set itemsToRemove; + double minAlphaLimit = ADVANCED_CFG::GetCfg().m_PcbSelectionVisibilityRatio; + double currentStackupOpacity = 0.0; + PCB_LAYER_ID lastVisibleLayer = PCB_LAYER_ID::UNDEFINED_LAYER; + + for( const LAYER_OPACITY_ITEM& opacityItem : opacityStackup ) + { + if( lastVisibleLayer == PCB_LAYER_ID::UNDEFINED_LAYER ) + { + currentStackupOpacity = opacityItem.m_Opacity; + lastVisibleLayer = opacityItem.m_Layer; + visibleItems.emplace( opacityItem.m_Item ); + continue; + } + + // Objects to ignore and fallback to the old selection behavior. + auto ignoreItem = [&]() + { + const BOARD_ITEM* item = opacityItem.m_Item; + + wxCHECK( item, false ); + + // Check items that span multiple layers for visibility. + if( visibleItems.count( item ) ) + return true; + + // Don't prune child items of a footprint that is already visible. + if( item->GetParent() + && ( item->GetParent()->Type() == PCB_FOOTPRINT_T ) + && visibleItems.count( item->GetParent() ) ) + return true; + + // Keepout zones are transparent but for some reason, + // PCB_PAINTER::GetColor() returns the color of the zone it + // prevents from filling. + if( isCopperPourKeepoutZone( item ) ) + return true; + + return false; + }; + + // Everything on the currently selected layer is visible; + if( opacityItem.m_Layer == enabledLayerStack[0] ) + { + visibleItems.emplace( opacityItem.m_Item ); + } + else + { + double itemVisibility = opacityItem.m_Opacity * ( 1.0 - currentStackupOpacity ); + + if( ( itemVisibility <= minAlphaLimit ) && !ignoreItem() ) + itemsToRemove.emplace( opacityItem.m_Item ); + else + visibleItems.emplace( opacityItem.m_Item ); + } + + if( opacityItem.m_Layer != lastVisibleLayer ) + { + currentStackupOpacity += opacityItem.m_Opacity * ( 1.0 - currentStackupOpacity ); + currentStackupOpacity = std::min( currentStackupOpacity, 1.0 ); + lastVisibleLayer = opacityItem.m_Layer; + } + } + + for( const BOARD_ITEM* itemToRemove : itemsToRemove ) + { + wxCHECK( aCollector.GetCount() > 1, /* void */ ); + aCollector.Remove( itemToRemove ); + } +} + + // The general idea here is that if the user clicks directly on a small item inside a larger // one, then they want the small item. The quintessential case of this is clicking on a pad // within a footprint, but we also apply it for text within a footprint, footprints within @@ -3087,6 +3251,12 @@ void PCB_SELECTION_TOOL::GuessSelectionCandidates( GENERAL_COLLECTOR& aCollector static const LSET silkLayers( 2, B_SilkS, F_SilkS ); static const LSET courtyardLayers( 2, B_CrtYd, F_CrtYd ); + if( ADVANCED_CFG::GetCfg().m_PcbSelectionVisibilityRatio != 1.0 ) + pruneObscuredSelectionCandidates( aCollector ); + + if( aCollector.GetCount() == 1 ) + return; + std::set preferred; std::set rejected; VECTOR2I where( aWhere.x, aWhere.y ); diff --git a/pcbnew/tools/pcb_selection_tool.h b/pcbnew/tools/pcb_selection_tool.h index df47fcc2af..87d38a2069 100644 --- a/pcbnew/tools/pcb_selection_tool.h +++ b/pcbnew/tools/pcb_selection_tool.h @@ -2,7 +2,7 @@ * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2013-2017 CERN - * Copyright (C) 2017-2022 KiCad Developers, see AUTHORS.TXT for contributors. + * Copyright (C) 2017-2023 KiCad Developers, see AUTHORS.TXT for contributors. * * @author Tomasz Wlostowski * @author Maciej Suminski @@ -422,6 +422,8 @@ private: */ int updateSelection( const TOOL_EVENT& aEvent ); + void pruneObscuredSelectionCandidates( GENERAL_COLLECTOR& aCollector ) const; + const GENERAL_COLLECTORS_GUIDE getCollectorsGuide() const; private: diff --git a/qa/tests/common/CMakeLists.txt b/qa/tests/common/CMakeLists.txt index 438106a7a2..877f7393ec 100644 --- a/qa/tests/common/CMakeLists.txt +++ b/qa/tests/common/CMakeLists.txt @@ -41,6 +41,7 @@ set( QA_COMMON_SRCS test_kicad_string.cpp test_kicad_stroke_font.cpp test_kiid.cpp + test_layer_ids.cpp test_property.cpp test_refdes_utils.cpp test_richio.cpp diff --git a/qa/tests/common/test_layer_ids.cpp b/qa/tests/common/test_layer_ids.cpp new file mode 100644 index 0000000000..0a6aa73874 --- /dev/null +++ b/qa/tests/common/test_layer_ids.cpp @@ -0,0 +1,45 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2023 Wayne Stambaugh + * Copyright (C) 2023 KiCad Developers, see AUTHORS.txt for contributors. + * + * 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 . + */ + +#include +#include + + +BOOST_AUTO_TEST_SUITE( LayerIds ) + + +BOOST_AUTO_TEST_CASE( LseqTestLayers ) +{ + LSET allLayers = LSET::AllLayersMask(); + LSEQ seq1 = allLayers.SeqStackupTop2Bottom(); + + BOOST_CHECK_EQUAL( seq1.TestLayers( PCB_LAYER_ID::F_Cu, PCB_LAYER_ID::F_Cu ), 0 ); + BOOST_CHECK_GT( seq1.TestLayers( PCB_LAYER_ID::F_Cu, PCB_LAYER_ID::In1_Cu ), 0 ); + BOOST_CHECK_LT( seq1.TestLayers( PCB_LAYER_ID::In1_Cu, PCB_LAYER_ID::F_Cu ), 0 ); + + // Pretend like inner copper layer one is the currently selected layer. + LSEQ seq2 = allLayers.SeqStackupTop2Bottom( PCB_LAYER_ID::In1_Cu ); + BOOST_CHECK_LT( seq2.TestLayers( PCB_LAYER_ID::F_Cu, PCB_LAYER_ID::In1_Cu ), 0 ); + BOOST_CHECK_GT( seq2.TestLayers( PCB_LAYER_ID::In1_Cu, PCB_LAYER_ID::F_Cu ), 0 ); +} + + +BOOST_AUTO_TEST_SUITE_END() +