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.
This commit is contained in:
Seth Hillbrand
2026-03-12 09:13:09 -07:00
parent b16cead02a
commit f3441c00a3
10 changed files with 464 additions and 0 deletions
+1
View File
@@ -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
@@ -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 <io/altium/altium_project_variants.h>
#include <wx/fileconf.h>
#include <wx/log.h>
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<int>( 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<ALTIUM_PROJECT_VARIANT> ParseAltiumProjectVariants( const wxString& aPrjPcbPath )
{
std::vector<ALTIUM_PROJECT_VARIANT> 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;
}
@@ -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 <map>
#include <vector>
#include <wx/string.h>
/**
* 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<wxString, wxString> 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<ALTIUM_VARIANT_ENTRY> 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<ALTIUM_PROJECT_VARIANT> ParseAltiumProjectVariants( const wxString& aPrjPcbPath );
#endif // ALTIUM_PROJECT_VARIANTS_H
+104
View File
@@ -29,6 +29,7 @@
#include <io/altium/altium_binary_parser.h>
#include <io/altium/altium_ascii_parser.h>
#include <io/altium/altium_parser_utils.h>
#include <io/altium/altium_project_variants.h>
#include <sch_io/altium/sch_io_altium.h>
#include <progress_reporter.h>
@@ -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::pair<wxString, const ALTIUM_VARIANT_ENTRY*>>;
std::map<wxString, ENTRY_LIST> variantsByUid;
std::map<wxString, ENTRY_LIST> 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<SCH_SYMBOL*>( 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::map<wxString, wxStrin
screen->Append( symbol );
m_symbols.insert( { aIndex, symbol } );
if( !elem.uniqueid.empty() )
m_altiumSymbolToUid[symbol] = elem.uniqueid;
}
+3
View File
@@ -260,6 +260,9 @@ private:
// List of available fonts with font name and font size in pt
std::vector<std::pair<wxString, int>> m_fonts;
// Persists across sheets so variant application can match by Altium UniqueId
std::map<const SCH_SYMBOL*, wxString> m_altiumSymbolToUid;
// Cache the error messages to avoid duplicate messages
std::unordered_map<wxString, SEVERITY > m_errorMessages;
};
@@ -34,6 +34,7 @@
#include <altium_pcb.h>
#include <altium_pcb_compound_file.h>
#include <io/altium/altium_binary_parser.h>
#include <io/altium/altium_project_variants.h>
#include <pcb_io/pcb_io.h>
#include <reporter.h>
@@ -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;
}
@@ -35,6 +35,7 @@
#include <altium_pcb.h>
#include <altium_pcb_compound_file.h>
#include <io/altium/altium_binary_parser.h>
#include <io/altium/altium_project_variants.h>
#include <pcb_io/pcb_io.h>
#include <reporter.h>
@@ -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;
}
@@ -34,14 +34,98 @@
#include <altium_pcb_compound_file.h>
#include <io/io_utils.h>
#include <io/altium/altium_binary_parser.h>
#include <io/altium/altium_project_variants.h>
#include <pcb_io/pcb_io.h>
#include <reporter.h>
#include <board.h>
#include <footprint.h>
#include <compoundfilereader.h>
#include <utf.h>
void ApplyAltiumProjectVariantsToBoard( BOARD* aBoard,
const std::vector<ALTIUM_PROJECT_VARIANT>& aVariants )
{
std::map<wxString, FOOTPRINT*> fpByRef;
std::map<KIID, FOOTPRINT*> 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;
}
@@ -31,8 +31,18 @@
#include <map>
#include <memory>
#include <vector>
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<ALTIUM_PROJECT_VARIANT>& aVariants );
class PCB_IO_ALTIUM_DESIGNER : public PCB_IO, public LAYER_MAPPABLE_PLUGIN
@@ -25,6 +25,7 @@
#include <altium_pcb.h>
#include <altium_pcb_compound_file.h>
#include <io/altium/altium_binary_parser.h>
#include <io/altium/altium_project_variants.h>
#include <pcb_io/pcb_io.h>
#include <reporter.h>
@@ -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;
}