diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 1e7b1ac5c2..6174828808 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -675,6 +675,7 @@ set( COMMON_IO_SRCS io/altium/altium_binary_parser.cpp io/altium/altium_ascii_parser.cpp io/altium/altium_parser_utils.cpp + io/altium/altium_project_variants.cpp io/altium/altium_props_utils.cpp # Cadstar diff --git a/common/io/altium/altium_project_variants.cpp b/common/io/altium/altium_project_variants.cpp new file mode 100644 index 0000000000..a81f4bce5d --- /dev/null +++ b/common/io/altium/altium_project_variants.cpp @@ -0,0 +1,159 @@ +/* + * 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 + + +static ALTIUM_VARIANT_ENTRY ParseVariationString( const wxString& aValue ) +{ + ALTIUM_VARIANT_ENTRY entry; + + for( const wxString& token : wxSplit( aValue, '|' ) ) + { + int eqPos = token.Find( '=' ); + + if( eqPos == wxNOT_FOUND ) + continue; + + wxString key = token.Left( eqPos ).Trim().Trim( false ); + wxString val = token.Mid( eqPos + 1 ).Trim().Trim( false ); + + if( key.CmpNoCase( wxS( "Designator" ) ) == 0 ) + { + entry.designator = val; + } + else if( key.CmpNoCase( wxS( "UniqueId" ) ) == 0 ) + { + entry.uniqueId = val; + } + else if( key.CmpNoCase( wxS( "Kind" ) ) == 0 ) + { + long k = 0; + val.ToLong( &k ); + entry.kind = static_cast( k ); + } + else if( key.CmpNoCase( wxS( "AlternatePart" ) ) == 0 ) + { + if( val.empty() ) + continue; + + // AlternatePart contains sub-fields in the same pipe-delimited format, but since + // it appears at the end of the line, the remaining tokens are its sub-fields. + // We've already split on '|', so we just keep accumulating from here. + // However, wxSplit already split everything. The AlternatePart sub-fields are the + // remaining tokens after this one. We handle this below after the loop. + } + } + + // Parse AlternatePart sub-fields. In the raw line, AlternatePart= is followed by + // pipe-separated key=value pairs that are part of the alternate part definition. + // Since we already split on '|', find the AlternatePart token and treat everything + // after it as sub-fields. + bool inAlternatePart = false; + + for( const wxString& token : wxSplit( aValue, '|' ) ) + { + int eqPos = token.Find( '=' ); + + if( eqPos == wxNOT_FOUND ) + continue; + + wxString key = token.Left( eqPos ).Trim().Trim( false ); + wxString val = token.Mid( eqPos + 1 ).Trim().Trim( false ); + + if( key.CmpNoCase( wxS( "AlternatePart" ) ) == 0 ) + { + inAlternatePart = true; + + // The value after AlternatePart= might itself be the first sub-field value + // In practice it's typically empty for Kind=1, or a lib reference for Kind=0 + if( !val.empty() ) + entry.alternateFields[wxS( "LibReference" )] = val; + + continue; + } + + if( inAlternatePart && !val.empty() ) + entry.alternateFields[key] = val; + } + + return entry; +} + + +std::vector ParseAltiumProjectVariants( const wxString& aPrjPcbPath ) +{ + std::vector variants; + + wxFileConfig config( wxEmptyString, wxEmptyString, wxEmptyString, aPrjPcbPath, + wxCONFIG_USE_NO_ESCAPE_CHARACTERS ); + + wxString groupname; + long groupid; + + for( bool more = config.GetFirstGroup( groupname, groupid ); more; + more = config.GetNextGroup( groupname, groupid ) ) + { + if( !groupname.StartsWith( wxS( "ProjectVariant" ) ) ) + continue; + + wxString numStr = groupname.Mid( 14 ); + long num; + + if( !numStr.ToLong( &num ) ) + continue; + + ALTIUM_PROJECT_VARIANT pv; + + pv.description = config.Read( groupname + wxS( "/Description" ), wxEmptyString ); + pv.name = pv.description; + + if( pv.name.empty() ) + pv.name = groupname; + + long variationCount = 0; + config.Read( groupname + wxS( "/VariationCount" ), &variationCount, 0 ); + + for( long vi = 1; vi <= variationCount; ++vi ) + { + wxString key = wxString::Format( wxS( "%s/Variation%ld" ), groupname, vi ); + wxString value = config.Read( key, wxEmptyString ); + + if( value.empty() ) + continue; + + ALTIUM_VARIANT_ENTRY entry = ParseVariationString( value ); + + if( !entry.designator.empty() ) + pv.variations.push_back( std::move( entry ) ); + } + + if( !pv.variations.empty() ) + variants.push_back( std::move( pv ) ); + } + + return variants; +} diff --git a/common/io/altium/altium_project_variants.h b/common/io/altium/altium_project_variants.h new file mode 100644 index 0000000000..dfa7ab3d5e --- /dev/null +++ b/common/io/altium/altium_project_variants.h @@ -0,0 +1,68 @@ +/* + * 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 + */ + +#ifndef ALTIUM_PROJECT_VARIANTS_H +#define ALTIUM_PROJECT_VARIANTS_H + +#include +#include +#include + +/** + * A single component variation within an Altium project variant. + * + * In Altium, each variant can override individual components. Kind=1 means + * "Not Fitted" (DNP), Kind=0 means "Alternate" with optional field overrides + * supplied via the AlternatePart string. + */ +struct ALTIUM_VARIANT_ENTRY +{ + wxString designator; + wxString uniqueId; + int kind = 0; + std::map alternateFields; +}; + + +/** + * A project-level assembly variant parsed from an Altium .PrjPcb file. + * + * Each [ProjectVariantN] section in the .PrjPcb becomes one of these. + */ +struct ALTIUM_PROJECT_VARIANT +{ + wxString name; + wxString description; + std::vector variations; +}; + + +/** + * Parse all [ProjectVariantN] sections from an Altium .PrjPcb project file. + * + * @param aPrjPcbPath Full path to the .PrjPcb file. + * @return Vector of project variants with their per-component entries. + */ +std::vector ParseAltiumProjectVariants( const wxString& aPrjPcbPath ); + +#endif // ALTIUM_PROJECT_VARIANTS_H diff --git a/eeschema/sch_io/altium/sch_io_altium.cpp b/eeschema/sch_io/altium/sch_io_altium.cpp index cf75309cef..1a96d75fd0 100644 --- a/eeschema/sch_io/altium/sch_io_altium.cpp +++ b/eeschema/sch_io/altium/sch_io_altium.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -639,6 +640,106 @@ SCH_SHEET* SCH_IO_ALTIUM::LoadSchematicFile( const wxString& aFileName, SCHEMATI allSheets.UpdateSymbolLinks( &LOAD_INFO_REPORTER::GetInstance() ); // Update all symbol library links for all sheets. allSheets.ClearEditFlags(); + // Apply Altium project variants to schematic symbols + if( aProperties && aProperties->count( "project_file" ) ) + { + auto variants = ParseAltiumProjectVariants( aProperties->at( "project_file" ) ); + + if( !variants.empty() ) + { + // Build lookups keyed by both UniqueId and designator. UniqueId is preferred + // because repeated-channel designs can have multiple components sharing a + // designator but with distinct UniqueIds. + using ENTRY_LIST = + std::vector>; + + std::map variantsByUid; + std::map variantsByDesignator; + + for( const ALTIUM_PROJECT_VARIANT& pv : variants ) + { + m_schematic->AddVariant( pv.name ); + + if( !pv.description.empty() && pv.description != pv.name ) + m_schematic->SetVariantDescription( pv.name, pv.description ); + + for( const ALTIUM_VARIANT_ENTRY& entry : pv.variations ) + { + if( !entry.uniqueId.empty() ) + variantsByUid[entry.uniqueId].push_back( { pv.name, &entry } ); + + variantsByDesignator[entry.designator].push_back( { pv.name, &entry } ); + } + } + + SCH_SHEET_LIST sheetList( m_rootSheet ); + + for( const SCH_SHEET_PATH& path : sheetList ) + { + SCH_SCREEN* screen = path.LastScreen(); + + if( !screen ) + continue; + + for( SCH_ITEM* item : screen->Items().OfType( SCH_SYMBOL_T ) ) + { + SCH_SYMBOL* symbol = static_cast( item ); + + const ENTRY_LIST* entries = nullptr; + + auto symUidIt = m_altiumSymbolToUid.find( symbol ); + + if( symUidIt != m_altiumSymbolToUid.end() ) + { + auto varIt = variantsByUid.find( symUidIt->second ); + + if( varIt != variantsByUid.end() ) + entries = &varIt->second; + } + + if( !entries ) + { + wxString ref = symbol->GetRef( &path ); + auto varIt = variantsByDesignator.find( ref ); + + if( varIt != variantsByDesignator.end() ) + entries = &varIt->second; + } + + if( !entries ) + continue; + + for( const auto& [variantName, entry] : *entries ) + { + SCH_SYMBOL_VARIANT variant( variantName ); + variant.InitializeAttributes( *symbol ); + + if( entry->kind == 1 ) + { + variant.m_DNP = true; + variant.m_ExcludedFromBOM = true; + variant.m_ExcludedFromPosFiles = true; + } + else if( entry->kind == 0 ) + { + for( const auto& [key, value] : entry->alternateFields ) + { + if( key.CmpNoCase( wxS( "LibReference" ) ) == 0 ) + variant.m_Fields[wxS( "Value" )] = value; + else if( key.CmpNoCase( wxS( "Description" ) ) == 0 ) + variant.m_Fields[wxS( "Description" )] = value; + else if( key.CmpNoCase( wxS( "Footprint" ) ) == 0 ) + variant.m_Fields[wxS( "Footprint" )] = value; + } + } + + symbol->AddVariant( path, variant ); + } + } + } + } + } + // Set up the default netclass wire & bus width based on imported wires & buses. // @@ -1730,6 +1831,9 @@ void SCH_IO_ALTIUM::ParseComponent( int aIndex, const std::mapAppend( symbol ); m_symbols.insert( { aIndex, symbol } ); + + if( !elem.uniqueid.empty() ) + m_altiumSymbolToUid[symbol] = elem.uniqueid; } diff --git a/eeschema/sch_io/altium/sch_io_altium.h b/eeschema/sch_io/altium/sch_io_altium.h index 26e9702b32..c0bed2ccd4 100644 --- a/eeschema/sch_io/altium/sch_io_altium.h +++ b/eeschema/sch_io/altium/sch_io_altium.h @@ -260,6 +260,9 @@ private: // List of available fonts with font name and font size in pt std::vector> m_fonts; + // Persists across sheets so variant application can match by Altium UniqueId + std::map m_altiumSymbolToUid; + // Cache the error messages to avoid duplicate messages std::unordered_map m_errorMessages; }; diff --git a/pcbnew/pcb_io/altium/pcb_io_altium_circuit_maker.cpp b/pcbnew/pcb_io/altium/pcb_io_altium_circuit_maker.cpp index af5b00c93a..b81e553dfb 100644 --- a/pcbnew/pcb_io/altium/pcb_io_altium_circuit_maker.cpp +++ b/pcbnew/pcb_io/altium/pcb_io_altium_circuit_maker.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include @@ -116,5 +117,13 @@ BOARD* PCB_IO_ALTIUM_CIRCUIT_MAKER::LoadBoard( const wxString& aFileName, BOARD* THROW_IO_ERROR( exception.what() ); } + if( m_props && m_props->count( "project_file" ) ) + { + auto variants = ParseAltiumProjectVariants( m_props->at( "project_file" ) ); + + if( !variants.empty() ) + ApplyAltiumProjectVariantsToBoard( m_board, variants ); + } + return m_board; } diff --git a/pcbnew/pcb_io/altium/pcb_io_altium_circuit_studio.cpp b/pcbnew/pcb_io/altium/pcb_io_altium_circuit_studio.cpp index e57b542a9d..a3b756f028 100644 --- a/pcbnew/pcb_io/altium/pcb_io_altium_circuit_studio.cpp +++ b/pcbnew/pcb_io/altium/pcb_io_altium_circuit_studio.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include @@ -117,5 +118,13 @@ BOARD* PCB_IO_ALTIUM_CIRCUIT_STUDIO::LoadBoard( const wxString& aFileName, BOARD THROW_IO_ERROR( exception.what() ); } + if( m_props && m_props->count( "project_file" ) ) + { + auto variants = ParseAltiumProjectVariants( m_props->at( "project_file" ) ); + + if( !variants.empty() ) + ApplyAltiumProjectVariantsToBoard( m_board, variants ); + } + return m_board; } diff --git a/pcbnew/pcb_io/altium/pcb_io_altium_designer.cpp b/pcbnew/pcb_io/altium/pcb_io_altium_designer.cpp index 437e553b47..4bbd984f2f 100644 --- a/pcbnew/pcb_io/altium/pcb_io_altium_designer.cpp +++ b/pcbnew/pcb_io/altium/pcb_io_altium_designer.cpp @@ -34,14 +34,98 @@ #include #include #include +#include #include #include #include +#include #include #include + +void ApplyAltiumProjectVariantsToBoard( BOARD* aBoard, + const std::vector& aVariants ) +{ + std::map fpByRef; + std::map fpByUid; + + for( FOOTPRINT* fp : aBoard->Footprints() ) + { + fpByRef[fp->GetReference()] = fp; + + // The Altium PCB importer stores sourceUniqueID as the last element of the + // footprint path. Use it to disambiguate repeated designators. + const KIID_PATH& path = fp->GetPath(); + + if( path.size() >= 2 ) + fpByUid[path.back()] = fp; + } + + for( const ALTIUM_PROJECT_VARIANT& pv : aVariants ) + { + aBoard->AddVariant( pv.name ); + + if( !pv.description.empty() && pv.description != pv.name ) + aBoard->SetVariantDescription( pv.name, pv.description ); + + for( const ALTIUM_VARIANT_ENTRY& entry : pv.variations ) + { + FOOTPRINT* target = nullptr; + + // Prefer UniqueId matching to handle repeated designators correctly + if( !entry.uniqueId.empty() ) + { + wxString normalizedUid = entry.uniqueId; + + if( normalizedUid.starts_with( wxT( "\\" ) ) ) + normalizedUid = normalizedUid.Mid( 1 ); + + auto it = fpByUid.find( KIID( normalizedUid ) ); + + if( it != fpByUid.end() ) + target = it->second; + } + + if( !target ) + { + auto it = fpByRef.find( entry.designator ); + + if( it != fpByRef.end() ) + target = it->second; + } + + if( !target ) + continue; + + FOOTPRINT_VARIANT* fpVariant = target->AddVariant( pv.name ); + + if( !fpVariant ) + continue; + + if( entry.kind == 1 ) + { + fpVariant->SetDNP( true ); + fpVariant->SetExcludedFromBOM( true ); + fpVariant->SetExcludedFromPosFiles( true ); + } + else if( entry.kind == 0 ) + { + for( const auto& [key, value] : entry.alternateFields ) + { + if( key.CmpNoCase( wxS( "LibReference" ) ) == 0 ) + fpVariant->SetFieldValue( wxS( "Value" ), value ); + else if( key.CmpNoCase( wxS( "Description" ) ) == 0 ) + fpVariant->SetFieldValue( wxS( "Description" ), value ); + else if( key.CmpNoCase( wxS( "Footprint" ) ) == 0 ) + fpVariant->SetFieldValue( wxS( "Footprint" ), value ); + } + } + } + } +} + PCB_IO_ALTIUM_DESIGNER::PCB_IO_ALTIUM_DESIGNER() : PCB_IO( wxS( "Altium Designer" ) ) { @@ -150,6 +234,14 @@ BOARD* PCB_IO_ALTIUM_DESIGNER::LoadBoard( const wxString& aFileName, BOARD* aApp THROW_IO_ERROR( exception.what() ); } + if( m_props && m_props->count( "project_file" ) ) + { + auto variants = ParseAltiumProjectVariants( m_props->at( "project_file" ) ); + + if( !variants.empty() ) + ApplyAltiumProjectVariantsToBoard( m_board, variants ); + } + return m_board; } diff --git a/pcbnew/pcb_io/altium/pcb_io_altium_designer.h b/pcbnew/pcb_io/altium/pcb_io_altium_designer.h index 006176d90f..3ec6b223b9 100644 --- a/pcbnew/pcb_io/altium/pcb_io_altium_designer.h +++ b/pcbnew/pcb_io/altium/pcb_io_altium_designer.h @@ -31,8 +31,18 @@ #include #include +#include class ALTIUM_PCB_COMPOUND_FILE; +class BOARD; +struct ALTIUM_PROJECT_VARIANT; + +/** + * Apply parsed Altium project variants to a board by setting FOOTPRINT_VARIANT + * data on each footprint whose reference matches a variant entry. + */ +void ApplyAltiumProjectVariantsToBoard( BOARD* aBoard, + const std::vector& aVariants ); class PCB_IO_ALTIUM_DESIGNER : public PCB_IO, public LAYER_MAPPABLE_PLUGIN diff --git a/pcbnew/pcb_io/altium/pcb_io_solidworks.cpp b/pcbnew/pcb_io/altium/pcb_io_solidworks.cpp index 2ffd86a75a..35eabef869 100644 --- a/pcbnew/pcb_io/altium/pcb_io_solidworks.cpp +++ b/pcbnew/pcb_io/altium/pcb_io_solidworks.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -136,5 +137,13 @@ BOARD* PCB_IO_SOLIDWORKS::LoadBoard( const wxString& aFileName, BOARD* aAppendTo THROW_IO_ERROR( exception.what() ); } + if( m_props && m_props->count( "project_file" ) ) + { + auto variants = ParseAltiumProjectVariants( m_props->at( "project_file" ) ); + + if( !variants.empty() ) + ApplyAltiumProjectVariantsToBoard( m_board, variants ); + } + return m_board; }