From f3441c00a39aee5525c996fba8de92690cc6f554 Mon Sep 17 00:00:00 2001 From: Seth Hillbrand Date: Thu, 12 Mar 2026 09:13:09 -0700 Subject: [PATCH] Import Altium project variants Parse variant data from Altium .PrjPcb project files and apply it to both schematic symbols and board footprints during import. Each ProjectVariant section is read as an INI group, and per-component variation entries are mapped to KiCad variant structures on the matching designators. --- common/CMakeLists.txt | 1 + common/io/altium/altium_project_variants.cpp | 159 ++++++++++++++++++ common/io/altium/altium_project_variants.h | 68 ++++++++ eeschema/sch_io/altium/sch_io_altium.cpp | 104 ++++++++++++ eeschema/sch_io/altium/sch_io_altium.h | 3 + .../altium/pcb_io_altium_circuit_maker.cpp | 9 + .../altium/pcb_io_altium_circuit_studio.cpp | 9 + .../pcb_io/altium/pcb_io_altium_designer.cpp | 92 ++++++++++ pcbnew/pcb_io/altium/pcb_io_altium_designer.h | 10 ++ pcbnew/pcb_io/altium/pcb_io_solidworks.cpp | 9 + 10 files changed, 464 insertions(+) create mode 100644 common/io/altium/altium_project_variants.cpp create mode 100644 common/io/altium/altium_project_variants.h 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; }