From 43e432f9adfda54b6f77217afc1db0d8595eb76a Mon Sep 17 00:00:00 2001 From: Mike Williams Date: Mon, 31 Mar 2025 08:54:49 -0400 Subject: [PATCH] sch_group: add basic class implementation + test --- eeschema/CMakeLists.txt | 1 + eeschema/sch_group.cpp | 435 +++++++++++++++++++++++++++++ eeschema/sch_group.h | 181 ++++++++++++ include/core/typeinfo.h | 2 + qa/tests/eeschema/test_ee_item.cpp | 16 ++ 5 files changed, 635 insertions(+) create mode 100644 eeschema/sch_group.cpp create mode 100644 eeschema/sch_group.h diff --git a/eeschema/CMakeLists.txt b/eeschema/CMakeLists.txt index 702adaac81..b430744176 100644 --- a/eeschema/CMakeLists.txt +++ b/eeschema/CMakeLists.txt @@ -384,6 +384,7 @@ set( EESCHEMA_SRCS sch_draw_panel.cpp sch_edit_frame.cpp sch_field.cpp + sch_group.cpp sch_item.cpp sch_junction.cpp sch_label.cpp diff --git a/eeschema/sch_group.cpp b/eeschema/sch_group.cpp new file mode 100644 index 0000000000..4dff41955d --- /dev/null +++ b/eeschema/sch_group.cpp @@ -0,0 +1,435 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The 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 2 + * 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, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +SCH_GROUP::SCH_GROUP() : SCH_ITEM( nullptr, SCH_GROUP_T ) +{ +} + +SCH_GROUP::SCH_GROUP( SCH_ITEM* aParent ) : SCH_ITEM( aParent, SCH_GROUP_T ) +{ +} + + +bool SCH_GROUP::IsGroupableType( KICAD_T aType ) +{ + switch( aType ) + { + case SCH_SYMBOL_T: + case SCH_PIN_T: + case SCH_SHAPE_T: + case SCH_BITMAP_T: + case SCH_FIELD_T: + case SCH_TEXT_T: + case SCH_TEXTBOX_T: + case SCH_TABLE_T: + case SCH_GROUP_T: + case SCH_LINE_T: + case SCH_JUNCTION_T: + case SCH_NO_CONNECT_T: + case SCH_BUS_WIRE_ENTRY_T: + case SCH_BUS_BUS_ENTRY_T: + case SCH_LABEL_T: + case SCH_GLOBAL_LABEL_T: + case SCH_HIER_LABEL_T: + case SCH_RULE_AREA_T: + case SCH_DIRECTIVE_LABEL_T: + case SCH_SHEET_PIN_T: + case SCH_SHEET_T: + return true; + default: + return false; + } +} + + +bool SCH_GROUP::AddItem( SCH_ITEM* aItem ) +{ + wxCHECK_MSG( IsGroupableType( aItem->Type() ), false, + wxT( "Invalid item type added to group: " ) + aItem->GetTypeDesc() ); + + // Items can only be in one group at a time + if( aItem->GetParentGroup() ) + aItem->GetParentGroup()->RemoveItem( aItem ); + + m_items.insert( aItem ); + aItem->SetParentGroup( this ); + return true; +} + + +bool SCH_GROUP::RemoveItem( SCH_ITEM* aItem ) +{ + // Only clear the item's group field if it was inside this group + if( m_items.erase( aItem ) == 1 ) + { + aItem->SetParentGroup( nullptr ); + return true; + } + + return false; +} + + +void SCH_GROUP::RemoveAll() +{ + for( SCH_ITEM* item : m_items ) + item->SetParentGroup( nullptr ); + + m_items.clear(); +} + + +/* + * @return if not in the symbol editor and aItem is in a symbol, returns the + * symbol's parent group. Otherwise, returns the aItem's parent group. + */ +SCH_GROUP* getClosestGroup( SCH_ITEM* aItem, bool isSymbolEditor ) +{ + if( !isSymbolEditor && aItem->GetParent() && aItem->GetParent()->Type() == SCH_SYMBOL_T ) + return static_cast( aItem->GetParent() )->GetParentGroup(); + else + return aItem->GetParentGroup(); +} + + +/// Returns the top level group inside the aScope group, or nullptr +SCH_GROUP* getNestedGroup( SCH_ITEM* aItem, SCH_GROUP* aScope, bool isSymbolEditor ) +{ + SCH_GROUP* group = getClosestGroup( aItem, isSymbolEditor ); + + if( group == aScope ) + return nullptr; + + while( group && group->GetParentGroup() && group->GetParentGroup() != aScope ) + { + if( group->GetParent()->Type() == LIB_SYMBOL_T && isSymbolEditor ) + break; + + group = group->GetParentGroup(); + } + + return group; +} + + +SCH_GROUP* SCH_GROUP::TopLevelGroup( SCH_ITEM* aItem, SCH_GROUP* aScope, bool isSymbolEditor ) +{ + return getNestedGroup( aItem, aScope, isSymbolEditor ); +} + + +bool SCH_GROUP::WithinScope( SCH_ITEM* aItem, SCH_GROUP* aScope, bool isSymbolEditor ) +{ + SCH_GROUP* group = getClosestGroup( aItem, isSymbolEditor ); + + if( group && group == aScope ) + return true; + + SCH_GROUP* nested = getNestedGroup( aItem, aScope, isSymbolEditor ); + + return nested && nested->GetParentGroup() && nested->GetParentGroup() == aScope; +} + + +VECTOR2I SCH_GROUP::GetPosition() const +{ + return GetBoundingBox().Centre(); +} + + +void SCH_GROUP::SetPosition( const VECTOR2I& aNewpos ) +{ + VECTOR2I delta = aNewpos - GetPosition(); + + Move( delta ); +} + + +EDA_ITEM* SCH_GROUP::Clone() const +{ + // Use copy constructor to get the same uuid and other fields + SCH_GROUP* newGroup = new SCH_GROUP( *this ); + return newGroup; +} + + +SCH_GROUP* SCH_GROUP::DeepClone() const +{ + // Use copy constructor to get the same uuid and other fields + SCH_GROUP* newGroup = new SCH_GROUP( *this ); + newGroup->m_items.clear(); + + for( SCH_ITEM* member : m_items ) + { + if( member->Type() == SCH_GROUP_T ) + newGroup->AddItem( static_cast( member )->DeepClone() ); + else + newGroup->AddItem( static_cast( member->Clone() ) ); + } + + return newGroup; +} + + +SCH_GROUP* SCH_GROUP::DeepDuplicate() const +{ + SCH_GROUP* newGroup = static_cast( Duplicate() ); + newGroup->m_items.clear(); + + for( SCH_ITEM* member : m_items ) + { + if( member->Type() == SCH_GROUP_T ) + newGroup->AddItem( static_cast( member )->DeepDuplicate() ); + else + newGroup->AddItem( static_cast( member->Duplicate() ) ); + } + + return newGroup; +} + + +void SCH_GROUP::swapData( SCH_ITEM* aImage ) +{ + assert( aImage->Type() == SCH_GROUP_T ); + + std::swap( *( (SCH_GROUP*) this ), *( (SCH_GROUP*) aImage ) ); +} + + +bool SCH_GROUP::HitTest( const VECTOR2I& aPosition, int aAccuracy ) const +{ + // Groups are selected by promoting a selection of one of their children + return false; +} + + +bool SCH_GROUP::HitTest( const BOX2I& aRect, bool aContained, int aAccuracy ) const +{ + // Groups are selected by promoting a selection of one of their children + return false; +} + + +const BOX2I SCH_GROUP::GetBoundingBox() const +{ + BOX2I bbox; + + for( SCH_ITEM* item : m_items ) + { + if( item->Type() == SCH_SYMBOL_T || item->Type() == LIB_SYMBOL_T ) + bbox.Merge( static_cast( item )->GetBoundingBox() ); + else + bbox.Merge( item->GetBoundingBox() ); + } + + bbox.Inflate( schIUScale.MilsToIU( 5 ) ); // Give a min size to the bbox + + return bbox; +} + + +INSPECT_RESULT SCH_GROUP::Visit( INSPECTOR aInspector, void* aTestData, + const std::vector& aScanTypes ) +{ + for( KICAD_T scanType : aScanTypes ) + { + if( scanType == Type() ) + { + if( INSPECT_RESULT::QUIT == aInspector( this, aTestData ) ) + return INSPECT_RESULT::QUIT; + } + } + + return INSPECT_RESULT::CONTINUE; +} + + +std::vector SCH_GROUP::ViewGetLayers() const +{ + return { LAYER_SCHEMATIC_ANCHOR }; +} + + +double SCH_GROUP::ViewGetLOD( int aLayer, const KIGFX::VIEW* aView ) const +{ + if( aView->IsLayerVisible( LAYER_SCHEMATIC_ANCHOR ) ) + return LOD_SHOW; + + return LOD_HIDE; +} + + +void SCH_GROUP::Move( const VECTOR2I& aMoveVector ) +{ + for( SCH_ITEM* member : m_items ) + member->Move( aMoveVector ); +} + + +void SCH_GROUP::Rotate( const VECTOR2I& aCenter, bool aRotateCCW ) +{ + for( SCH_ITEM* member : m_items ) + member->Rotate( aCenter, aRotateCCW ); +} + + +void SCH_GROUP::MirrorHorizontally( int aCenter ) +{ + for( SCH_ITEM* item : m_items ) + item->MirrorHorizontally( aCenter ); +} + + +void SCH_GROUP::MirrorVertically( int aCenter ) +{ + for( SCH_ITEM* item : m_items ) + item->MirrorVertically( aCenter ); +} + + +wxString SCH_GROUP::GetItemDescription( UNITS_PROVIDER* aUnitsProvider, bool aFull ) const +{ + if( m_name.empty() ) + return wxString::Format( _( "Unnamed Group, %zu members" ), m_items.size() ); + else + return wxString::Format( _( "Group '%s', %zu members" ), m_name, m_items.size() ); +} + + +BITMAPS SCH_GROUP::GetMenuImage() const +{ + return BITMAPS::module; +} + + +void SCH_GROUP::GetMsgPanelInfo( EDA_DRAW_FRAME* aFrame, std::vector& aList ) +{ + aList.emplace_back( _( "Group" ), m_name.empty() ? _( "" ) : m_name ); + aList.emplace_back( _( "Members" ), wxString::Format( wxT( "%zu" ), m_items.size() ) ); +} + + +void SCH_GROUP::RunOnChildren( const std::function& aFunction, RECURSE_MODE aMode ) +{ + try + { + for( SCH_ITEM* item : m_items ) + { + aFunction( item ); + + if( item->Type() == SCH_GROUP_T ) + item->RunOnChildren( aFunction, RECURSE_MODE::RECURSE ); + } + } + catch( std::bad_function_call& ) + { + wxFAIL_MSG( wxT( "Error calling function in SCH_GROUP::RunOnChildren" ) ); + } +} + + +bool SCH_GROUP::operator==( const SCH_ITEM& aSchItem ) const +{ + if( aSchItem.Type() != Type() ) + return false; + + const SCH_GROUP& other = static_cast( aSchItem ); + + return *this == other; +} + + +bool SCH_GROUP::operator==( const SCH_GROUP& aOther ) const +{ + if( m_items.size() != aOther.m_items.size() ) + return false; + + // The items in groups are in unordered sets hashed by the pointer value, so we need to + // order them by UUID (EDA_ITEM_SET) to compare + EDA_ITEM_SET itemSet( m_items.begin(), m_items.end() ); + EDA_ITEM_SET otherItemSet( aOther.m_items.begin(), aOther.m_items.end() ); + + for( auto it1 = itemSet.begin(), it2 = otherItemSet.begin(); it1 != itemSet.end(); ++it1, ++it2 ) + { + // Compare UUID instead of the items themselves because we only care if the contents + // of the group has changed, not which elements in the group have changed + if( ( *it1 )->m_Uuid != ( *it2 )->m_Uuid ) + return false; + } + + return true; +} + + +double SCH_GROUP::Similarity( const SCH_ITEM& aOther ) const +{ + if( aOther.Type() != Type() ) + return 0.0; + + const SCH_GROUP& other = static_cast( aOther ); + + double similarity = 0.0; + + for( SCH_ITEM* item : m_items ) + { + for( SCH_ITEM* otherItem : other.m_items ) + { + similarity += item->Similarity( *otherItem ); + } + } + + return similarity / m_items.size(); +} + + +static struct SCH_GROUP_DESC +{ + SCH_GROUP_DESC() + { + PROPERTY_MANAGER& propMgr = PROPERTY_MANAGER::Instance(); + REGISTER_TYPE( SCH_GROUP ); + propMgr.AddTypeCast( new TYPE_CAST ); + propMgr.InheritsAfter( TYPE_HASH( SCH_GROUP ), TYPE_HASH( SCH_ITEM ) ); + + propMgr.Mask( TYPE_HASH( SCH_GROUP ), TYPE_HASH( SCH_ITEM ), _HKI( "Position X" ) ); + propMgr.Mask( TYPE_HASH( SCH_GROUP ), TYPE_HASH( SCH_ITEM ), _HKI( "Position Y" ) ); + + const wxString groupTab = _HKI( "Group Properties" ); + + propMgr.AddProperty( + new PROPERTY( _HKI( "Name" ), &SCH_GROUP::SetName, &SCH_GROUP::GetName ), + groupTab ); + } +} _SCH_GROUP_DESC; diff --git a/eeschema/sch_group.h b/eeschema/sch_group.h new file mode 100644 index 0000000000..5d092d5a20 --- /dev/null +++ b/eeschema/sch_group.h @@ -0,0 +1,181 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The 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 2 + * 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, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/** + * @file sch_group.h + * @brief Class to handle a set of SCH_ITEMs. + */ + +#ifndef CLASS_SCH_GROUP_H_ +#define CLASS_SCH_GROUP_H_ + +#include +#include +#include +#include + +namespace KIGFX +{ +class VIEW; +} + +/** + * A set of SCH_ITEMs (i.e., without duplicates). + * + * The group parent is always sheet, not logical parent group. The group is transparent + * container - e.g., its position is derived from the position of its members. A selection + * containing a group implicitly contains its members. However other operations on sets of + * items, like committing, updating the view, etc the set is explicit. + */ +class SCH_GROUP : public SCH_ITEM +{ +public: + SCH_GROUP(); + + SCH_GROUP( SCH_ITEM* aParent ); + + static inline bool ClassOf( const EDA_ITEM* aItem ) { return aItem && SCH_GROUP_T == aItem->Type(); } + + wxString GetClass() const override { return wxT( "SCH_GROUP" ); } + + wxString GetName() const { return m_name; } + void SetName( const wxString& aName ) { m_name = aName; } + + std::unordered_set& GetItems() { return m_items; } + + const std::unordered_set& GetItems() const { return m_items; } + + /** + * Add item to group. Does not take ownership of item. + * + * @return true if item was added (false if item belongs to a different group). + */ + virtual bool AddItem( SCH_ITEM* aItem ); + + /** + * Remove item from group. + * + * @return true if item was removed (false if item was not in the group). + */ + virtual bool RemoveItem( SCH_ITEM* aItem ); + + void RemoveAll(); + + /* + * Search for highest level group inside of aScope, containing item. + * + * @param aScope restricts the search to groups within the group scope. + * @param isSymbolEditor true if we should stop promoting at the symbol level + * @return group inside of aScope, containing item, if exists, otherwise, nullptr + */ + static SCH_GROUP* TopLevelGroup( SCH_ITEM* aItem, SCH_GROUP* aScope, bool isSymbolEditor ); + + static bool WithinScope( SCH_ITEM* aItem, SCH_GROUP* aScope, bool isSymbolEditor ); + + double Similarity( const SCH_ITEM& aOther ) const override; + + bool operator==( const SCH_GROUP& aOther ) const; + bool operator==( const SCH_ITEM& aSchItem ) const override; + +#if defined( DEBUG ) + void Show( int nestLevel, std::ostream& os ) const override { ShowDummy( os ); } +#endif + + /// @copydoc EDA_ITEM::GetPosition + VECTOR2I GetPosition() const override; + + /// @copydoc EDA_ITEM::SetPosition + void SetPosition( const VECTOR2I& aNewpos ) override; + + /// @copydoc EDA_ITEM::Clone + EDA_ITEM* Clone() const override; + + /* + * Clone() this and all descendants + */ + SCH_GROUP* DeepClone() const; + + /* + * Duplicate() this and all descendants + */ + SCH_GROUP* DeepDuplicate() const; + + /// @copydoc EDA_ITEM::HitTest + bool HitTest( const VECTOR2I& aPosition, int aAccuracy = 0 ) const override; + + /// @copydoc EDA_ITEM::HitTest + bool HitTest( const BOX2I& aRect, bool aContained, int aAccuracy = 0 ) const override; + + /// @copydoc EDA_ITEM::GetBoundingBox + const BOX2I GetBoundingBox() const override; + + /// @copydoc EDA_ITEM::Visit + INSPECT_RESULT Visit( INSPECTOR aInspector, void* aTestData, + const std::vector& aScanTypes ) override; + + /// @copydoc VIEW_ITEM::ViewGetLayers + std::vector ViewGetLayers() const override; + + /// @copydoc VIEW_ITEM::ViewGetLOD + double ViewGetLOD( int aLayer, const KIGFX::VIEW* aView ) const override; + + /// @copydoc SCH_ITEM::Move + void Move( const VECTOR2I& aMoveVector ) override; + + /// @copydoc SCH_ITEM::Rotate + void Rotate( const VECTOR2I& aCenter, bool aRotateCCW ) override; + + /// @copydoc SCH_ITEM::MirrorHorizontally + void MirrorHorizontally( int aCenter ) override; + + /// @copydoc SCH_ITEM::MirrorVertically + void MirrorVertically( int aCenter ) override; + + /// @copydoc EDA_ITEM::GetItemDescription + wxString GetItemDescription( UNITS_PROVIDER* aUnitsProvider, bool aFull ) const override; + + /// @copydoc EDA_ITEM::GetMenuImage + BITMAPS GetMenuImage() const override; + + /// @copydoc EDA_ITEM::GetMsgPanelInfo + void GetMsgPanelInfo( EDA_DRAW_FRAME* aFrame, std::vector& aList ) override; + + ///< @copydoc SCH_ITEM::RunOnChildren + void RunOnChildren( const std::function& aFunction, RECURSE_MODE aMode ) override; + + /** + * Check if the proposed type can be added to a group + * @param aType KICAD_T type to check + * @return true if the type can belong to a group, false otherwise + */ + static bool IsGroupableType( KICAD_T aType ); + + /// @copydoc SCH_ITEM::swapData + void swapData( SCH_ITEM* aImage ) override; + +protected: + std::unordered_set m_items; // Members of the group + wxString m_name; // Optional group name +}; + +#endif diff --git a/include/core/typeinfo.h b/include/core/typeinfo.h index 936340b001..c12c1f1dec 100644 --- a/include/core/typeinfo.h +++ b/include/core/typeinfo.h @@ -170,6 +170,7 @@ enum KICAD_T SCH_RULE_AREA_T, SCH_DIRECTIVE_LABEL_T, SCH_SYMBOL_T, + SCH_GROUP_T, SCH_SHEET_PIN_T, SCH_SHEET_T, @@ -388,6 +389,7 @@ constexpr bool IsEeschemaType( const KICAD_T aType ) case SCH_FIELD_T: case SCH_SYMBOL_T: case SCH_SHEET_PIN_T: + case SCH_GROUP_T: case SCH_SHEET_T: case SCH_PIN_T: diff --git a/qa/tests/eeschema/test_ee_item.cpp b/qa/tests/eeschema/test_ee_item.cpp index 91c99197fe..7f5a7a83b1 100644 --- a/qa/tests/eeschema/test_ee_item.cpp +++ b/qa/tests/eeschema/test_ee_item.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -54,6 +55,7 @@ public: SCH_SHEET m_sheet; LIB_SYMBOL m_symbol; SCH_PIN m_pin; + SCH_TEXT m_text; std::shared_ptr m_ercItem; TEST_EE_ITEM_FIXTURE() : @@ -66,6 +68,11 @@ public: m_sheet.SetSize( VECTOR2I( schIUScale.mmToIU( 50 ), schIUScale.mmToIU( 100 ) ) ); } + ~TEST_EE_ITEM_FIXTURE() + { + m_text.SetParentGroup( nullptr ); + } + EDA_ITEM* Instantiate( KICAD_T aType ) { if( !IsEeschemaType( aType ) ) @@ -132,6 +139,15 @@ public: case SCH_SHEET_T: return new SCH_SHEET(); case SCH_PIN_T: return new SCH_PIN( &m_symbol ); + case SCH_GROUP_T: + { + SCH_GROUP* group = new SCH_GROUP(); + + // Group position only makes sense if there's at least one item in the group. + group->AddItem( &m_text ); + + return group; + } case SCHEMATIC_T: case LIB_SYMBOL_T: return nullptr;