/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2019-2020 Thomas Pointhuber * Copyright (C) 2021-2024 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 "altium_pcb.h" #include "altium_parser_pcb.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include constexpr double BOLD_FACTOR = 1.75; // CSS font-weight-normal is 400; bold is 700 bool IsAltiumLayerCopper( ALTIUM_LAYER aLayer ) { return ( aLayer >= ALTIUM_LAYER::TOP_LAYER && aLayer <= ALTIUM_LAYER::BOTTOM_LAYER ) || aLayer == ALTIUM_LAYER::MULTI_LAYER; // TODO: add IsAltiumLayerAPlane? } bool IsAltiumLayerAPlane( ALTIUM_LAYER aLayer ) { return aLayer >= ALTIUM_LAYER::INTERNAL_PLANE_1 && aLayer <= ALTIUM_LAYER::INTERNAL_PLANE_16; } FOOTPRINT* ALTIUM_PCB::HelperGetFootprint( uint16_t aComponent ) const { if( aComponent == ALTIUM_COMPONENT_NONE || m_components.size() <= aComponent ) { THROW_IO_ERROR( wxString::Format( wxT( "Component creator tries to access component id %u " "of %u existing components" ), (unsigned)aComponent, (unsigned)m_components.size() ) ); } return m_components.at( aComponent ); } void HelperShapeLineChainFromAltiumVertices( SHAPE_LINE_CHAIN& aLine, const std::vector& aVertices ) { for( const ALTIUM_VERTICE& vertex : aVertices ) { if( vertex.isRound ) { EDA_ANGLE angle( vertex.endangle - vertex.startangle, DEGREES_T ); angle.Normalize(); double startradiant = DEG2RAD( vertex.startangle ); double endradiant = DEG2RAD( vertex.endangle ); VECTOR2I arcStartOffset = VECTOR2I( KiROUND( std::cos( startradiant ) * vertex.radius ), -KiROUND( std::sin( startradiant ) * vertex.radius ) ); VECTOR2I arcEndOffset = VECTOR2I( KiROUND( std::cos( endradiant ) * vertex.radius ), -KiROUND( std::sin( endradiant ) * vertex.radius ) ); VECTOR2I arcStart = vertex.center + arcStartOffset; VECTOR2I arcEnd = vertex.center + arcEndOffset; if( GetLineLength( arcStart, vertex.position ) < GetLineLength( arcEnd, vertex.position ) ) { aLine.Append( SHAPE_ARC( vertex.center, arcStart, -angle ) ); } else { aLine.Append( SHAPE_ARC( vertex.center, arcEnd, angle ) ); } } else { aLine.Append( vertex.position ); } } aLine.SetClosed( true ); } PCB_LAYER_ID ALTIUM_PCB::GetKicadLayer( ALTIUM_LAYER aAltiumLayer ) const { auto override = m_layermap.find( aAltiumLayer ); if( override != m_layermap.end() ) { return override->second; } switch( aAltiumLayer ) { case ALTIUM_LAYER::UNKNOWN: return UNDEFINED_LAYER; case ALTIUM_LAYER::TOP_LAYER: return F_Cu; case ALTIUM_LAYER::MID_LAYER_1: return In1_Cu; case ALTIUM_LAYER::MID_LAYER_2: return In2_Cu; case ALTIUM_LAYER::MID_LAYER_3: return In3_Cu; case ALTIUM_LAYER::MID_LAYER_4: return In4_Cu; case ALTIUM_LAYER::MID_LAYER_5: return In5_Cu; case ALTIUM_LAYER::MID_LAYER_6: return In6_Cu; case ALTIUM_LAYER::MID_LAYER_7: return In7_Cu; case ALTIUM_LAYER::MID_LAYER_8: return In8_Cu; case ALTIUM_LAYER::MID_LAYER_9: return In9_Cu; case ALTIUM_LAYER::MID_LAYER_10: return In10_Cu; case ALTIUM_LAYER::MID_LAYER_11: return In11_Cu; case ALTIUM_LAYER::MID_LAYER_12: return In12_Cu; case ALTIUM_LAYER::MID_LAYER_13: return In13_Cu; case ALTIUM_LAYER::MID_LAYER_14: return In14_Cu; case ALTIUM_LAYER::MID_LAYER_15: return In15_Cu; case ALTIUM_LAYER::MID_LAYER_16: return In16_Cu; case ALTIUM_LAYER::MID_LAYER_17: return In17_Cu; case ALTIUM_LAYER::MID_LAYER_18: return In18_Cu; case ALTIUM_LAYER::MID_LAYER_19: return In19_Cu; case ALTIUM_LAYER::MID_LAYER_20: return In20_Cu; case ALTIUM_LAYER::MID_LAYER_21: return In21_Cu; case ALTIUM_LAYER::MID_LAYER_22: return In22_Cu; case ALTIUM_LAYER::MID_LAYER_23: return In23_Cu; case ALTIUM_LAYER::MID_LAYER_24: return In24_Cu; case ALTIUM_LAYER::MID_LAYER_25: return In25_Cu; case ALTIUM_LAYER::MID_LAYER_26: return In26_Cu; case ALTIUM_LAYER::MID_LAYER_27: return In27_Cu; case ALTIUM_LAYER::MID_LAYER_28: return In28_Cu; case ALTIUM_LAYER::MID_LAYER_29: return In29_Cu; case ALTIUM_LAYER::MID_LAYER_30: return In30_Cu; case ALTIUM_LAYER::BOTTOM_LAYER: return B_Cu; case ALTIUM_LAYER::TOP_OVERLAY: return F_SilkS; case ALTIUM_LAYER::BOTTOM_OVERLAY: return B_SilkS; case ALTIUM_LAYER::TOP_PASTE: return F_Paste; case ALTIUM_LAYER::BOTTOM_PASTE: return B_Paste; case ALTIUM_LAYER::TOP_SOLDER: return F_Mask; case ALTIUM_LAYER::BOTTOM_SOLDER: return B_Mask; case ALTIUM_LAYER::INTERNAL_PLANE_1: return UNDEFINED_LAYER; case ALTIUM_LAYER::INTERNAL_PLANE_2: return UNDEFINED_LAYER; case ALTIUM_LAYER::INTERNAL_PLANE_3: return UNDEFINED_LAYER; case ALTIUM_LAYER::INTERNAL_PLANE_4: return UNDEFINED_LAYER; case ALTIUM_LAYER::INTERNAL_PLANE_5: return UNDEFINED_LAYER; case ALTIUM_LAYER::INTERNAL_PLANE_6: return UNDEFINED_LAYER; case ALTIUM_LAYER::INTERNAL_PLANE_7: return UNDEFINED_LAYER; case ALTIUM_LAYER::INTERNAL_PLANE_8: return UNDEFINED_LAYER; case ALTIUM_LAYER::INTERNAL_PLANE_9: return UNDEFINED_LAYER; case ALTIUM_LAYER::INTERNAL_PLANE_10: return UNDEFINED_LAYER; case ALTIUM_LAYER::INTERNAL_PLANE_11: return UNDEFINED_LAYER; case ALTIUM_LAYER::INTERNAL_PLANE_12: return UNDEFINED_LAYER; case ALTIUM_LAYER::INTERNAL_PLANE_13: return UNDEFINED_LAYER; case ALTIUM_LAYER::INTERNAL_PLANE_14: return UNDEFINED_LAYER; case ALTIUM_LAYER::INTERNAL_PLANE_15: return UNDEFINED_LAYER; case ALTIUM_LAYER::INTERNAL_PLANE_16: return UNDEFINED_LAYER; case ALTIUM_LAYER::DRILL_GUIDE: return Dwgs_User; case ALTIUM_LAYER::KEEP_OUT_LAYER: return Margin; case ALTIUM_LAYER::MECHANICAL_1: return User_1; //Edge_Cuts; case ALTIUM_LAYER::MECHANICAL_2: return User_2; case ALTIUM_LAYER::MECHANICAL_3: return User_3; case ALTIUM_LAYER::MECHANICAL_4: return User_4; case ALTIUM_LAYER::MECHANICAL_5: return User_5; case ALTIUM_LAYER::MECHANICAL_6: return User_6; case ALTIUM_LAYER::MECHANICAL_7: return User_7; case ALTIUM_LAYER::MECHANICAL_8: return User_8; case ALTIUM_LAYER::MECHANICAL_9: return User_9; case ALTIUM_LAYER::MECHANICAL_10: return Dwgs_User; case ALTIUM_LAYER::MECHANICAL_11: return Eco2_User; //Eco1 is used for unknown elements case ALTIUM_LAYER::MECHANICAL_12: return F_Fab; case ALTIUM_LAYER::MECHANICAL_13: return B_Fab; // Don't use courtyard layers for other purposes case ALTIUM_LAYER::MECHANICAL_14: return UNDEFINED_LAYER; case ALTIUM_LAYER::MECHANICAL_15: return UNDEFINED_LAYER; case ALTIUM_LAYER::MECHANICAL_16: return UNDEFINED_LAYER; case ALTIUM_LAYER::DRILL_DRAWING: return Dwgs_User; case ALTIUM_LAYER::MULTI_LAYER: return UNDEFINED_LAYER; case ALTIUM_LAYER::CONNECTIONS: return UNDEFINED_LAYER; case ALTIUM_LAYER::BACKGROUND: return UNDEFINED_LAYER; case ALTIUM_LAYER::DRC_ERROR_MARKERS: return UNDEFINED_LAYER; case ALTIUM_LAYER::SELECTIONS: return UNDEFINED_LAYER; case ALTIUM_LAYER::VISIBLE_GRID_1: return UNDEFINED_LAYER; case ALTIUM_LAYER::VISIBLE_GRID_2: return UNDEFINED_LAYER; case ALTIUM_LAYER::PAD_HOLES: return UNDEFINED_LAYER; case ALTIUM_LAYER::VIA_HOLES: return UNDEFINED_LAYER; default: return UNDEFINED_LAYER; } } std::vector ALTIUM_PCB::GetKicadLayersToIterate( ALTIUM_LAYER aAltiumLayer ) const { static std::set altiumLayersWithWarning; if( aAltiumLayer == ALTIUM_LAYER::MULTI_LAYER || aAltiumLayer == ALTIUM_LAYER::KEEP_OUT_LAYER ) { std::vector layers; layers.reserve( MAX_CU_LAYERS ); for( PCB_LAYER_ID layer : LSET::AllCuMask().Seq() ) { if( !m_board || m_board->IsLayerEnabled( layer ) ) layers.emplace_back( layer ); } return layers; } PCB_LAYER_ID klayer = GetKicadLayer( aAltiumLayer ); if( klayer == UNDEFINED_LAYER ) { wxLogWarning( _( "Altium layer (%d) has no KiCad equivalent. It has been moved to KiCad " "layer Eco1_User." ), aAltiumLayer ); klayer = Eco1_User; } return { klayer }; } ALTIUM_PCB::ALTIUM_PCB( BOARD* aBoard, PROGRESS_REPORTER* aProgressReporter, const wxString& aLibrary, const wxString& aFootprintName ) { m_board = aBoard; m_progressReporter = aProgressReporter; m_doneCount = 0; m_lastProgressCount = 0; m_totalCount = 0; m_highest_pour_index = 0; m_library = aLibrary; m_footprintName = aFootprintName; } ALTIUM_PCB::~ALTIUM_PCB() { } void ALTIUM_PCB::checkpoint() { const unsigned PROGRESS_DELTA = 250; if( m_progressReporter ) { if( ++m_doneCount > m_lastProgressCount + PROGRESS_DELTA ) { m_progressReporter->SetCurrentProgress( ( (double) m_doneCount ) / std::max( 1U, m_totalCount ) ); if( !m_progressReporter->KeepRefreshing() ) THROW_IO_ERROR( _( "Open cancelled by user." ) ); m_lastProgressCount = m_doneCount; } } } void ALTIUM_PCB::Parse( const ALTIUM_COMPOUND_FILE& altiumPcbFile, const std::map& aFileMapping ) { // this vector simply declares in which order which functions to call. const std::vector> parserOrder = { { true, ALTIUM_PCB_DIR::FILE_HEADER, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseFileHeader( aFile, fileHeader ); } }, { true, ALTIUM_PCB_DIR::BOARD6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseBoard6Data( aFile, fileHeader ); } }, { false, ALTIUM_PCB_DIR::EXTENDPRIMITIVEINFORMATION, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseExtendedPrimitiveInformationData( aFile, fileHeader ); } }, { true, ALTIUM_PCB_DIR::COMPONENTS6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseComponents6Data( aFile, fileHeader ); } }, { false, ALTIUM_PCB_DIR::MODELS, [this, aFileMapping]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { std::vector dir{ aFileMapping.at( ALTIUM_PCB_DIR::MODELS ) }; this->ParseModelsData( aFile, fileHeader, dir ); } }, { true, ALTIUM_PCB_DIR::COMPONENTBODIES6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseComponentsBodies6Data( aFile, fileHeader ); } }, { true, ALTIUM_PCB_DIR::NETS6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseNets6Data( aFile, fileHeader ); } }, { true, ALTIUM_PCB_DIR::CLASSES6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseClasses6Data( aFile, fileHeader ); } }, { true, ALTIUM_PCB_DIR::RULES6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseRules6Data( aFile, fileHeader ); } }, { true, ALTIUM_PCB_DIR::DIMENSIONS6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseDimensions6Data( aFile, fileHeader ); } }, { true, ALTIUM_PCB_DIR::POLYGONS6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParsePolygons6Data( aFile, fileHeader ); } }, { true, ALTIUM_PCB_DIR::ARCS6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseArcs6Data( aFile, fileHeader ); } }, { true, ALTIUM_PCB_DIR::PADS6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParsePads6Data( aFile, fileHeader ); } }, { true, ALTIUM_PCB_DIR::VIAS6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseVias6Data( aFile, fileHeader ); } }, { true, ALTIUM_PCB_DIR::TRACKS6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseTracks6Data( aFile, fileHeader ); } }, { false, ALTIUM_PCB_DIR::WIDESTRINGS6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseWideStrings6Data( aFile, fileHeader ); } }, { true, ALTIUM_PCB_DIR::TEXTS6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseTexts6Data( aFile, fileHeader ); } }, { true, ALTIUM_PCB_DIR::FILLS6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseFills6Data( aFile, fileHeader ); } }, { false, ALTIUM_PCB_DIR::BOARDREGIONS, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseBoardRegionsData( aFile, fileHeader ); } }, { true, ALTIUM_PCB_DIR::SHAPEBASEDREGIONS6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseShapeBasedRegions6Data( aFile, fileHeader ); } }, { true, ALTIUM_PCB_DIR::REGIONS6, [this]( const ALTIUM_COMPOUND_FILE& aFile, auto fileHeader ) { this->ParseRegions6Data( aFile, fileHeader ); } } }; if( m_progressReporter != nullptr ) { // Count number of records we will read for the progress reporter for( const std::tuple& cur : parserOrder ) { bool isRequired; ALTIUM_PCB_DIR directory; PARSE_FUNCTION_POINTER_fp fp; std::tie( isRequired, directory, fp ) = cur; if( directory == ALTIUM_PCB_DIR::FILE_HEADER ) continue; const auto& mappedDirectory = aFileMapping.find( directory ); if( mappedDirectory == aFileMapping.end() ) continue; const std::vector mappedFile{ mappedDirectory->second, "Header" }; const CFB::COMPOUND_FILE_ENTRY* file = altiumPcbFile.FindStream( mappedFile ); if( file == nullptr ) continue; ALTIUM_BINARY_PARSER reader( altiumPcbFile, file ); uint32_t numOfRecords = reader.Read(); if( reader.HasParsingError() ) { wxLogError( _( "'%s' was not parsed correctly." ), FormatPath( mappedFile ) ); continue; } m_totalCount += numOfRecords; if( reader.GetRemainingBytes() != 0 ) { wxLogError( _( "'%s' was not fully parsed." ), FormatPath( mappedFile ) ); continue; } } } const auto& boardDirectory = aFileMapping.find( ALTIUM_PCB_DIR::BOARD6 ); if( boardDirectory != aFileMapping.end() ) { std::vector mappedFile{ boardDirectory->second, "Data" }; const CFB::COMPOUND_FILE_ENTRY* file = altiumPcbFile.FindStream( mappedFile ); if( !file ) { THROW_IO_ERROR( _( "This file does not appear to be in a valid PCB Binary Version 6.0 format. In " "Altium Designer, " "make sure to save as \"PCB Binary Files (*.PcbDoc)\"." ) ); } } // Parse data in specified order for( const std::tuple& cur : parserOrder ) { bool isRequired; ALTIUM_PCB_DIR directory; PARSE_FUNCTION_POINTER_fp fp; std::tie( isRequired, directory, fp ) = cur; const auto& mappedDirectory = aFileMapping.find( directory ); if( mappedDirectory == aFileMapping.end() ) { wxASSERT_MSG( !isRequired, wxString::Format( wxT( "Altium Directory of kind %d was " "expected, but no mapping is " "present in the code" ), directory ) ); continue; } std::vector mappedFile{ mappedDirectory->second }; if( directory != ALTIUM_PCB_DIR::FILE_HEADER ) mappedFile.emplace_back( "Data" ); const CFB::COMPOUND_FILE_ENTRY* file = altiumPcbFile.FindStream( mappedFile ); if( file != nullptr ) { fp( altiumPcbFile, file ); } else if( isRequired ) { wxLogError( _( "File not found: '%s' for directory '%s'." ), FormatPath( mappedFile ), magic_enum::enum_name( directory ) ); } } // fixup zone priorities since Altium stores them in the opposite order for( ZONE* zone : m_polygons ) { if( !zone ) continue; // Altium "fills" - not poured in Altium if( zone->GetAssignedPriority() == 1000 ) { // Unlikely, but you never know if( m_highest_pour_index >= 1000 ) zone->SetAssignedPriority( m_highest_pour_index + 1 ); continue; } int priority = m_highest_pour_index - zone->GetAssignedPriority(); zone->SetAssignedPriority( priority >= 0 ? priority : 0 ); } // change priority of outer zone to zero for( std::pair& zone : m_outer_plane ) zone.second->SetAssignedPriority( 0 ); // Simplify and fracture zone fills in case we constructed them from tracks (hatched fill) for( ZONE* zone : m_polygons ) { if( !zone ) continue; for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() ) { if( !zone->HasFilledPolysForLayer( layer ) ) continue; zone->GetFilledPolysList( layer )->Fracture( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); } } // Altium doesn't appear to store either the dimension value nor the dimensioned object in // the dimension record. (Yes, there is a REFERENCE0OBJECTID, but it doesn't point to the // dimensioned object.) We attempt to plug this gap by finding a colocated arc or circle // and using its radius. If there are more than one such arcs/circles, well, :shrug:. for( PCB_DIM_RADIAL* dim : m_radialDimensions ) { int radius = 0; for( BOARD_ITEM* item : m_board->Drawings() ) { if( item->Type() != PCB_SHAPE_T ) continue; PCB_SHAPE* shape = static_cast( item ); if( shape->GetShape() != SHAPE_T::ARC && shape->GetShape() != SHAPE_T::CIRCLE ) continue; if( shape->GetPosition() == dim->GetPosition() ) { radius = shape->GetRadius(); break; } } if( radius == 0 ) { for( PCB_TRACK* track : m_board->Tracks() ) { if( track->Type() != PCB_ARC_T ) continue; PCB_ARC* arc = static_cast( track ); if( arc->GetCenter() == dim->GetPosition() ) { radius = arc->GetRadius(); break; } } } // Move the radius point onto the circumference VECTOR2I radialLine = dim->GetEnd() - dim->GetStart(); int totalLength = radialLine.EuclideanNorm(); // Enforce a minimum on the radialLine else we won't have enough precision to get the // angle from it. radialLine = radialLine.Resize( std::max( radius, 2 ) ); dim->SetEnd( dim->GetStart() + (VECTOR2I) radialLine ); dim->SetLeaderLength( totalLength - radius ); dim->Update(); } // center board BOX2I bbbox = m_board->GetBoardEdgesBoundingBox(); int w = m_board->GetPageSettings().GetWidthIU( pcbIUScale.IU_PER_MILS ); int h = m_board->GetPageSettings().GetHeightIU( pcbIUScale.IU_PER_MILS ); int desired_x = ( w - bbbox.GetWidth() ) / 2; int desired_y = ( h - bbbox.GetHeight() ) / 2; VECTOR2I movementVector( desired_x - bbbox.GetX(), desired_y - bbbox.GetY() ); m_board->Move( movementVector ); BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings(); bds.SetAuxOrigin( bds.GetAuxOrigin() + movementVector ); bds.SetGridOrigin( bds.GetGridOrigin() + movementVector ); m_board->SetModified(); } FOOTPRINT* ALTIUM_PCB::ParseFootprint( ALTIUM_COMPOUND_FILE& altiumLibFile, const wxString& aFootprintName ) { std::unique_ptr footprint = std::make_unique( m_board ); // TODO: what should we do with those layers? m_layermap.emplace( ALTIUM_LAYER::MECHANICAL_14, Eco2_User ); m_layermap.emplace( ALTIUM_LAYER::MECHANICAL_15, Eco2_User ); m_layermap.emplace( ALTIUM_LAYER::MECHANICAL_16, Eco2_User ); m_unicodeStrings.clear(); m_extendedPrimitiveInformationMaps.clear(); // TODO: WideStrings are stored as parameterMap in the case of footprints, not as binary // std::string unicodeStringsStreamName = aFootprintName.ToStdString() + "\\WideStrings"; // const CFB::COMPOUND_FILE_ENTRY* unicodeStringsData = altiumLibFile.FindStream( unicodeStringsStreamName ); // if( unicodeStringsData != nullptr ) // { // ParseWideStrings6Data( altiumLibFile, unicodeStringsData ); // } std::tuple ret = altiumLibFile.FindLibFootprintDirName(aFootprintName); wxString fpDirName = std::get<0>( ret ); const CFB::COMPOUND_FILE_ENTRY* footprintStream = std::get<1>( ret ); if( fpDirName.IsEmpty() ) { THROW_IO_ERROR( wxString::Format( _( "Footprint directory not found: '%s'." ), aFootprintName ) ); } const std::vector streamName{ fpDirName.ToStdString(), "Data" }; const CFB::COMPOUND_FILE_ENTRY* footprintData = altiumLibFile.FindStream( footprintStream, { "Data" } ); if( footprintData == nullptr ) { THROW_IO_ERROR( wxString::Format( _( "File not found: '%s'." ), FormatPath( streamName ) ) ); } ALTIUM_BINARY_PARSER parser( altiumLibFile, footprintData ); parser.ReadAndSetSubrecordLength(); //wxString footprintName = parser.ReadWxString(); // Not used (single-byte char set) parser.SkipSubrecord(); LIB_ID fpID = AltiumToKiCadLibID( "", aFootprintName ); // TODO: library name footprint->SetFPID( fpID ); const std::vector parametersStreamName{ fpDirName.ToStdString(), "Parameters" }; const CFB::COMPOUND_FILE_ENTRY* parametersData = altiumLibFile.FindStream( footprintStream, { "Parameters" } ); if( parametersData != nullptr ) { ALTIUM_BINARY_PARSER parametersReader( altiumLibFile, parametersData ); std::map parameterProperties = parametersReader.ReadProperties(); wxString description = ALTIUM_PROPS_UTILS::ReadString( parameterProperties, wxT( "DESCRIPTION" ), wxT( "" ) ); footprint->SetLibDescription( description ); } else { wxLogError( _( "File not found: '%s'." ), FormatPath( parametersStreamName ) ); footprint->SetLibDescription( wxT( "" ) ); } const std::vector extendedPrimitiveInformationStreamName{ "ExtendedPrimitiveInformation", "Data" }; const CFB::COMPOUND_FILE_ENTRY* extendedPrimitiveInformationData = altiumLibFile.FindStream( footprintStream, extendedPrimitiveInformationStreamName ); if( extendedPrimitiveInformationData != nullptr ) ParseExtendedPrimitiveInformationData( altiumLibFile, extendedPrimitiveInformationData ); footprint->SetReference( wxT( "REF**" ) ); footprint->SetValue( aFootprintName ); footprint->Reference().SetVisible( true ); // TODO: extract visibility information footprint->Value().SetVisible( true ); const VECTOR2I defaultTextSize( pcbIUScale.mmToIU( 1.0 ), pcbIUScale.mmToIU( 1.0 ) ); const int defaultTextThickness( pcbIUScale.mmToIU( 0.15 ) ); for( PCB_FIELD* field : footprint->Fields() ) { field->SetTextSize( defaultTextSize ); field->SetTextThickness( defaultTextThickness ); } for( int primitiveIndex = 0; parser.GetRemainingBytes() >= 4; primitiveIndex++ ) { ALTIUM_RECORD recordtype = static_cast( parser.Peek() ); switch( recordtype ) { case ALTIUM_RECORD::ARC: { AARC6 arc( parser ); ConvertArcs6ToFootprintItem( footprint.get(), arc, primitiveIndex, false ); break; } case ALTIUM_RECORD::PAD: { APAD6 pad( parser ); ConvertPads6ToFootprintItem( footprint.get(), pad ); break; } case ALTIUM_RECORD::VIA: { AVIA6 via( parser ); // TODO: implement break; } case ALTIUM_RECORD::TRACK: { ATRACK6 track( parser ); ConvertTracks6ToFootprintItem( footprint.get(), track, primitiveIndex, false ); break; } case ALTIUM_RECORD::TEXT: { ATEXT6 text( parser, m_unicodeStrings ); ConvertTexts6ToFootprintItem( footprint.get(), text ); break; } case ALTIUM_RECORD::FILL: { AFILL6 fill( parser ); ConvertFills6ToFootprintItem( footprint.get(), fill, false ); break; } case ALTIUM_RECORD::REGION: { AREGION6 region( parser, false ); ConvertShapeBasedRegions6ToFootprintItem( footprint.get(), region, primitiveIndex ); break; } case ALTIUM_RECORD::MODEL: { ACOMPONENTBODY6 componentBody( parser ); // Won't be supported for now, as we would need to extract the model break; } default: THROW_IO_ERROR( wxString::Format( _( "Record of unknown type: '%d'." ), recordtype ) ); } } // Auto-position reference and value footprint->AutoPositionFields(); if( parser.HasParsingError() ) { THROW_IO_ERROR( wxString::Format( wxT( "%s stream was not parsed correctly" ), FormatPath( streamName ) ) ); } if( parser.GetRemainingBytes() != 0 ) { THROW_IO_ERROR( wxString::Format( wxT( "%s stream is not fully parsed" ), FormatPath( streamName ) ) ); } return footprint.release(); } int ALTIUM_PCB::GetNetCode( uint16_t aId ) const { if( aId == ALTIUM_NET_UNCONNECTED ) { return NETINFO_LIST::UNCONNECTED; } else if( m_altiumToKicadNetcodes.size() < aId ) { THROW_IO_ERROR( wxString::Format( wxT( "Netcode with id %d does not exist. Only %d nets " "are known" ), aId, m_altiumToKicadNetcodes.size() ) ); } else { return m_altiumToKicadNetcodes[ aId ]; } } const ARULE6* ALTIUM_PCB::GetRule( ALTIUM_RULE_KIND aKind, const wxString& aName ) const { const auto rules = m_rules.find( aKind ); if( rules == m_rules.end() ) return nullptr; for( const ARULE6& rule : rules->second ) { if( rule.name == aName ) return &rule; } return nullptr; } const ARULE6* ALTIUM_PCB::GetRuleDefault( ALTIUM_RULE_KIND aKind ) const { const auto rules = m_rules.find( aKind ); if( rules == m_rules.end() ) return nullptr; for( const ARULE6& rule : rules->second ) { if( rule.scope1expr == wxT( "All" ) && rule.scope2expr == wxT( "All" ) ) return &rule; } return nullptr; } void ALTIUM_PCB::ParseFileHeader( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); reader.ReadAndSetSubrecordLength(); wxString header = reader.ReadWxString(); //std::cout << "HEADER: " << header << std::endl; // tells me: PCB 5.0 Binary File //reader.SkipSubrecord(); // TODO: does not seem to work all the time at the moment //if( reader.GetRemainingBytes() != 0 ) // THROW_IO_ERROR( "FileHeader stream is not fully parsed" ); } void ALTIUM_PCB::ParseExtendedPrimitiveInformationData( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading extended primitive information data..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); while( reader.GetRemainingBytes() >= 4 /* TODO: use Header section of file */ ) { checkpoint(); AEXTENDED_PRIMITIVE_INFORMATION elem( reader ); m_extendedPrimitiveInformationMaps[elem.primitiveObjectId].emplace( elem.primitiveIndex, std::move( elem ) ); } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( wxT( "ExtendedPrimitiveInformation stream is not fully parsed" ) ); } void ALTIUM_PCB::ParseBoard6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading board data..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); checkpoint(); ABOARD6 elem( reader ); if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( wxT( "Board6 stream is not fully parsed" ) ); m_board->GetDesignSettings().SetAuxOrigin( elem.sheetpos ); m_board->GetDesignSettings().SetGridOrigin( elem.sheetpos ); // read layercount from stackup, because LAYERSETSCOUNT is not always correct?! size_t layercount = 0; size_t layerid = static_cast( ALTIUM_LAYER::TOP_LAYER ); while( layerid < elem.stackup.size() && layerid != 0 ) { layerid = elem.stackup[ layerid - 1 ].nextId; layercount++; } size_t kicadLayercount = ( layercount % 2 == 0 ) ? layercount : layercount + 1; m_board->SetCopperLayerCount( kicadLayercount ); BOARD_DESIGN_SETTINGS& designSettings = m_board->GetDesignSettings(); BOARD_STACKUP& stackup = designSettings.GetStackupDescriptor(); // create board stackup stackup.RemoveAll(); // Just to be sure stackup.BuildDefaultStackupList( &designSettings, layercount ); auto it = stackup.GetList().begin(); // find first copper layer for( ; it != stackup.GetList().end() && ( *it )->GetType() != BS_ITEM_TYPE_COPPER; ++it ) ; auto curLayer = static_cast( F_Cu ); for( size_t altiumLayerId = static_cast( ALTIUM_LAYER::TOP_LAYER ); altiumLayerId < elem.stackup.size() && altiumLayerId != 0; altiumLayerId = elem.stackup[altiumLayerId - 1].nextId ) { // array starts with 0, but stackup with 1 ABOARD6_LAYER_STACKUP& layer = elem.stackup.at( altiumLayerId - 1 ); // handle unused layer in case of odd layercount if( layer.nextId == 0 && layercount != kicadLayercount ) { m_board->SetLayerName( ( *it )->GetBrdLayerId(), wxT( "[unused]" ) ); if( ( *it )->GetType() != BS_ITEM_TYPE_COPPER ) THROW_IO_ERROR( wxT( "Board6 stream, unexpected item while parsing stackup" ) ); ( *it )->SetThickness( 0 ); ++it; if( ( *it )->GetType() != BS_ITEM_TYPE_DIELECTRIC ) THROW_IO_ERROR( wxT( "Board6 stream, unexpected item while parsing stackup" ) ); ( *it )->SetThickness( 0, 0 ); ( *it )->SetThicknessLocked( true, 0 ); ++it; } m_layermap.insert( { static_cast( altiumLayerId ), static_cast( curLayer++ ) } ); if( ( *it )->GetType() != BS_ITEM_TYPE_COPPER ) THROW_IO_ERROR( wxT( "Board6 stream, unexpected item while parsing stackup" ) ); ( *it )->SetThickness( layer.copperthick ); ALTIUM_LAYER alayer = static_cast( altiumLayerId ); PCB_LAYER_ID klayer = ( *it )->GetBrdLayerId(); m_board->SetLayerName( klayer, layer.name ); if( layer.copperthick == 0 ) m_board->SetLayerType( klayer, LAYER_T::LT_JUMPER ); // used for things like wirebonding else if( IsAltiumLayerAPlane( alayer ) ) m_board->SetLayerType( klayer, LAYER_T::LT_POWER ); if( klayer == B_Cu ) { if( layer.nextId != 0 ) THROW_IO_ERROR( wxT( "Board6 stream, unexpected id while parsing last stackup layer" ) ); // overwrite entry from internal -> bottom m_layermap[alayer] = B_Cu; break; } ++it; if( ( *it )->GetType() != BS_ITEM_TYPE_DIELECTRIC ) THROW_IO_ERROR( wxT( "Board6 stream, unexpected item while parsing stackup" ) ); ( *it )->SetThickness( layer.dielectricthick, 0 ); ( *it )->SetMaterial( layer.dielectricmaterial.empty() ? NotSpecifiedPrm() : wxString( layer.dielectricmaterial ) ); ( *it )->SetEpsilonR( layer.dielectricconst, 0 ); ++it; } // Set name of all non-cu layers for( size_t altiumLayerId = static_cast( ALTIUM_LAYER::TOP_OVERLAY ); altiumLayerId <= static_cast( ALTIUM_LAYER::BOTTOM_SOLDER ); altiumLayerId++ ) { // array starts with 0, but stackup with 1 ABOARD6_LAYER_STACKUP& layer = elem.stackup.at( altiumLayerId - 1 ); ALTIUM_LAYER alayer = static_cast( altiumLayerId ); PCB_LAYER_ID klayer = GetKicadLayer( alayer ); m_board->SetLayerName( klayer, layer.name ); } for( size_t altiumLayerId = static_cast( ALTIUM_LAYER::MECHANICAL_1 ); altiumLayerId <= static_cast( ALTIUM_LAYER::MECHANICAL_16 ); altiumLayerId++ ) { // array starts with 0, but stackup with 1 ABOARD6_LAYER_STACKUP& layer = elem.stackup.at( altiumLayerId - 1 ); ALTIUM_LAYER alayer = static_cast( altiumLayerId ); PCB_LAYER_ID klayer = GetKicadLayer( alayer ); m_board->SetLayerName( klayer, layer.name ); } HelperCreateBoardOutline( elem.board_vertices ); } void ALTIUM_PCB::HelperCreateBoardOutline( const std::vector& aVertices ) { SHAPE_LINE_CHAIN lineChain; HelperShapeLineChainFromAltiumVertices( lineChain, aVertices ); STROKE_PARAMS stroke( m_board->GetDesignSettings().GetLineThickness( Edge_Cuts ), LINE_STYLE::SOLID ); for( int i = 0; i <= lineChain.PointCount() && i != -1; i = lineChain.NextShape( i ) ) { if( lineChain.IsArcStart( i ) ) { const SHAPE_ARC& currentArc = lineChain.Arc( lineChain.ArcIndex( i ) ); int nextShape = lineChain.NextShape( i ); bool isLastShape = nextShape < 0; std::unique_ptr shape = std::make_unique( m_board, SHAPE_T::ARC ); shape->SetStroke( stroke ); shape->SetLayer( Edge_Cuts ); shape->SetArcGeometry( currentArc.GetP0(), currentArc.GetArcMid(), currentArc.GetP1() ); m_board->Add( shape.release(), ADD_MODE::APPEND ); } else { const SEG& seg = lineChain.Segment( i ); std::unique_ptr shape = std::make_unique( m_board, SHAPE_T::SEGMENT ); shape->SetStroke( stroke ); shape->SetLayer( Edge_Cuts ); shape->SetStart( seg.A ); shape->SetEnd( seg.B ); m_board->Add( shape.release(), ADD_MODE::APPEND ); } } } void ALTIUM_PCB::ParseClasses6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading netclasses..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); while( reader.GetRemainingBytes() >= 4 /* TODO: use Header section of file */ ) { checkpoint(); ACLASS6 elem( reader ); if( elem.kind == ALTIUM_CLASS_KIND::NET_CLASS ) { std::shared_ptr nc = std::make_shared( elem.name ); for( const wxString& name : elem.names ) { m_board->GetDesignSettings().m_NetSettings->m_NetClassPatternAssignments.push_back( { std::make_unique( name, CTX_NETCLASS ), nc->GetName() } ); } if( m_board->GetDesignSettings().m_NetSettings->m_NetClasses.count( nc->GetName() ) ) { // Name conflict, happens in some unknown circumstances // unique_ptr will delete nc on this code path wxLogWarning( _( "More than one Altium netclass with name '%s' found. " "Only the first one will be imported." ), elem.name ); } else { m_board->GetDesignSettings().m_NetSettings->m_NetClasses[ nc->GetName() ] = nc; } } } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( wxT( "Classes6 stream is not fully parsed" ) ); m_board->m_LegacyNetclassesLoaded = true; } void ALTIUM_PCB::ParseComponents6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading components..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); uint16_t componentId = 0; while( reader.GetRemainingBytes() >= 4 /* TODO: use Header section of file */ ) { checkpoint(); ACOMPONENT6 elem( reader ); std::unique_ptr footprint = std::make_unique( m_board ); LIB_ID fpID = AltiumToKiCadLibID( elem.sourcefootprintlibrary, elem.pattern ); footprint->SetFPID( fpID ); footprint->SetPosition( elem.position ); footprint->SetOrientationDegrees( elem.rotation ); // KiCad netlisting requires parts to have non-digit + digit annotation. // If the reference begins with a number, we prepend 'UNK' (unknown) for the source designator wxString reference = elem.sourcedesignator; if( reference.find_first_not_of( "0123456789" ) == wxString::npos ) reference.Prepend( wxT( "UNK" ) ); footprint->SetReference( reference ); footprint->SetLocked( elem.locked ); footprint->Reference().SetVisible( elem.nameon ); footprint->Value().SetVisible( elem.commenton ); footprint->SetLayer( elem.layer == ALTIUM_LAYER::TOP_LAYER ? F_Cu : B_Cu ); m_components.emplace_back( footprint.get() ); m_board->Add( footprint.release(), ADD_MODE::APPEND ); componentId++; } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( wxT( "Components6 stream is not fully parsed" ) ); } /// Normalize angle to be aMin < angle <= aMax angle is in degrees. double normalizeAngleDegrees( double Angle, double aMin, double aMax ) { while( Angle < aMin ) Angle += 360.0; while( Angle >= aMax ) Angle -= 360.0; return Angle; } void ALTIUM_PCB::ParseComponentsBodies6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading component 3D models..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); while( reader.GetRemainingBytes() >= 4 /* TODO: use Header section of file */ ) { checkpoint(); ACOMPONENTBODY6 elem( reader ); // TODO: implement if( elem.component == ALTIUM_COMPONENT_NONE ) continue; // TODO: we do not support components for the board yet if( m_components.size() <= elem.component ) { THROW_IO_ERROR( wxString::Format( wxT( "ComponentsBodies6 stream tries to access " "component id %d of %d existing components" ), elem.component, m_components.size() ) ); } if( !elem.modelIsEmbedded ) continue; auto modelTuple = m_models.find( elem.modelId ); if( modelTuple == m_models.end() ) { wxLogError( wxT( "ComponentsBodies6 stream tries to access model id %s which does not " "exist" ), elem.modelId ); continue; } FOOTPRINT* footprint = m_components.at( elem.component ); const VECTOR2I& fpPosition = footprint->GetPosition(); FP_3DMODEL modelSettings; modelSettings.m_Filename = modelTuple->second; modelSettings.m_Offset.x = pcbIUScale.IUTomm((int) elem.modelPosition.x - fpPosition.x ); modelSettings.m_Offset.y = -pcbIUScale.IUTomm((int) elem.modelPosition.y - fpPosition.y ); modelSettings.m_Offset.z = pcbIUScale.IUTomm( (int) elem.modelPosition.z ); EDA_ANGLE orientation = footprint->GetOrientation(); if( footprint->IsFlipped() ) { modelSettings.m_Offset.y = -modelSettings.m_Offset.y; orientation = -orientation; } RotatePoint( &modelSettings.m_Offset.x, &modelSettings.m_Offset.y, orientation ); modelSettings.m_Rotation.x = normalizeAngleDegrees( -elem.modelRotation.x, -180, 180 ); modelSettings.m_Rotation.y = normalizeAngleDegrees( -elem.modelRotation.y, -180, 180 ); modelSettings.m_Rotation.z = normalizeAngleDegrees( -elem.modelRotation.z + elem.rotation + orientation.AsDegrees(), -180, 180 ); modelSettings.m_Opacity = elem.bodyOpacity; footprint->Models().push_back( modelSettings ); } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( wxT( "ComponentsBodies6 stream is not fully parsed" ) ); } void ALTIUM_PCB::HelperParseDimensions6Linear( const ADIMENSION6& aElem ) { if( aElem.referencePoint.size() != 2 ) THROW_IO_ERROR( wxT( "Incorrect number of reference points for linear dimension object" ) ); PCB_LAYER_ID klayer = GetKicadLayer( aElem.layer ); if( klayer == UNDEFINED_LAYER ) { wxLogWarning( _( "Dimension found on an Altium layer (%d) with no KiCad equivalent. " "It has been moved to KiCad layer Eco1_User." ), aElem.layer ); klayer = Eco1_User; } VECTOR2I referencePoint0 = aElem.referencePoint.at( 0 ); VECTOR2I referencePoint1 = aElem.referencePoint.at( 1 ); std::unique_ptr dimension = std::make_unique( m_board, PCB_DIM_ALIGNED_T ); dimension->SetPrecision( static_cast( aElem.textprecision ) ); dimension->SetLayer( klayer ); dimension->SetStart( referencePoint0 ); if( referencePoint0 != aElem.xy1 ) { /** * Basically REFERENCE0POINT and REFERENCE1POINT are the two end points of the dimension. * XY1 is the position of the arrow above REFERENCE0POINT. those three points are not * necessarily in 90degree angle, but KiCad requires this to show the correct measurements. * * Therefore, we take the vector of REFERENCE0POINT -> XY1, calculate the normal, and * intersect it with REFERENCE1POINT pointing the same direction as REFERENCE0POINT -> XY1. * This should give us a valid measurement point where we can place the drawsegment. */ VECTOR2I direction = aElem.xy1 - referencePoint0; VECTOR2I directionNormalVector = VECTOR2I( -direction.y, direction.x ); SEG segm1( referencePoint0, referencePoint0 + directionNormalVector ); SEG segm2( referencePoint1, referencePoint1 + direction ); OPT_VECTOR2I intersection( segm1.Intersect( segm2, true, true ) ); if( !intersection ) THROW_IO_ERROR( wxT( "Invalid dimension. This should never happen." ) ); dimension->SetEnd( *intersection ); int height = static_cast( EuclideanNorm( direction ) ); if( ( direction.x > 0 || direction.y < 0 ) != ( aElem.angle >= 180.0 ) ) height = -height; dimension->SetHeight( height ); } else { dimension->SetEnd( referencePoint1 ); } dimension->SetLineThickness( aElem.linewidth ); dimension->SetUnitsFormat( DIM_UNITS_FORMAT::NO_SUFFIX ); dimension->SetPrefix( aElem.textprefix ); // Suffix normally (but not always) holds the units wxRegEx units( wxS( "(mm)|(in)|(mils)|(thou)|(')|(\")" ), wxRE_ADVANCED ); if( units.Matches( aElem.textsuffix ) ) dimension->SetUnitsFormat( DIM_UNITS_FORMAT::BARE_SUFFIX ); else dimension->SetSuffix( aElem.textsuffix ); dimension->SetTextThickness( aElem.textlinewidth ); dimension->SetTextSize( VECTOR2I( aElem.textheight, aElem.textheight ) ); dimension->SetItalic( aElem.textitalic ); #if 0 // we don't currently support bold; map to thicker text dimension->Text().SetBold( aElem.textbold ); #else if( aElem.textbold ) dimension->SetTextThickness( dimension->GetTextThickness() * BOLD_FACTOR ); #endif switch( aElem.textunit ) { case ALTIUM_UNIT::INCHES: dimension->SetUnits( EDA_UNITS::INCHES ); break; case ALTIUM_UNIT::MILS: dimension->SetUnits( EDA_UNITS::MILS ); break; case ALTIUM_UNIT::MILLIMETERS: case ALTIUM_UNIT::CENTIMETER: dimension->SetUnits( EDA_UNITS::MILLIMETRES ); break; default: break; } m_board->Add( dimension.release(), ADD_MODE::APPEND ); } void ALTIUM_PCB::HelperParseDimensions6Radial(const ADIMENSION6 &aElem) { if( aElem.referencePoint.size() < 2 ) THROW_IO_ERROR( wxT( "Not enough reference points for radial dimension object" ) ); PCB_LAYER_ID klayer = GetKicadLayer( aElem.layer ); if( klayer == UNDEFINED_LAYER ) { wxLogWarning( _( "Dimension found on an Altium layer (%d) with no KiCad equivalent. " "It has been moved to KiCad layer Eco1_User." ), aElem.layer ); klayer = Eco1_User; } VECTOR2I referencePoint0 = aElem.referencePoint.at( 0 ); VECTOR2I referencePoint1 = aElem.referencePoint.at( 1 ); std::unique_ptr dimension = std::make_unique( m_board ); dimension->SetPrecision( static_cast( aElem.textprecision ) ); dimension->SetLayer( klayer ); dimension->SetStart( referencePoint0 ); dimension->SetEnd( aElem.xy1 ); dimension->SetLineThickness( aElem.linewidth ); dimension->SetKeepTextAligned( false ); dimension->SetPrefix( aElem.textprefix ); // Suffix normally holds the units dimension->SetUnitsFormat( aElem.textsuffix.IsEmpty() ? DIM_UNITS_FORMAT::NO_SUFFIX : DIM_UNITS_FORMAT::BARE_SUFFIX ); switch( aElem.textunit ) { case ALTIUM_UNIT::INCHES: dimension->SetUnits( EDA_UNITS::INCHES ); break; case ALTIUM_UNIT::MILS: dimension->SetUnits( EDA_UNITS::MILS ); break; case ALTIUM_UNIT::MILLIMETERS: case ALTIUM_UNIT::CENTIMETER: dimension->SetUnits( EDA_UNITS::MILLIMETRES ); break; default: break; } if( aElem.textPoint.empty() ) { wxLogError( wxT( "No text position present for leader dimension object" ) ); return; } dimension->SetTextPos( aElem.textPoint.at( 0 ) ); dimension->SetTextThickness( aElem.textlinewidth ); dimension->SetTextSize( VECTOR2I( aElem.textheight, aElem.textheight ) ); dimension->SetItalic( aElem.textitalic ); #if 0 // we don't currently support bold; map to thicker text dimension->SetBold( aElem.textbold ); #else if( aElem.textbold ) dimension->SetTextThickness( dimension->GetTextThickness() * BOLD_FACTOR ); #endif // It's unclear exactly how Altium figures it's text positioning, but this gets us reasonably // close. dimension->SetVertJustify( GR_TEXT_V_ALIGN_BOTTOM ); dimension->SetHorizJustify( GR_TEXT_H_ALIGN_LEFT ); int yAdjust = dimension->GetTextBox().GetCenter().y - dimension->GetTextPos().y; dimension->SetTextPos( dimension->GetTextPos() + VECTOR2I( 0, yAdjust + aElem.textgap ) ); dimension->SetVertJustify( GR_TEXT_V_ALIGN_CENTER ); m_radialDimensions.push_back( dimension.get() ); m_board->Add( dimension.release(), ADD_MODE::APPEND ); } void ALTIUM_PCB::HelperParseDimensions6Leader( const ADIMENSION6& aElem ) { PCB_LAYER_ID klayer = GetKicadLayer( aElem.layer ); if( klayer == UNDEFINED_LAYER ) { wxLogWarning( _( "Dimension found on an Altium layer (%d) with no KiCad equivalent. " "It has been moved to KiCad layer Eco1_User." ), aElem.layer ); klayer = Eco1_User; } if( !aElem.referencePoint.empty() ) { VECTOR2I referencePoint0 = aElem.referencePoint.at( 0 ); // line VECTOR2I last = referencePoint0; for( size_t i = 1; i < aElem.referencePoint.size(); i++ ) { std::unique_ptr shape = std::make_unique( m_board, SHAPE_T::SEGMENT ); shape->SetLayer( klayer ); shape->SetStroke( STROKE_PARAMS( aElem.linewidth, LINE_STYLE::SOLID ) ); shape->SetStart( last ); shape->SetEnd( aElem.referencePoint.at( i ) ); last = aElem.referencePoint.at( i ); m_board->Add( shape.release(), ADD_MODE::APPEND ); } // arrow if( aElem.referencePoint.size() >= 2 ) { VECTOR2I dirVec = aElem.referencePoint.at( 1 ) - referencePoint0; if( dirVec.x != 0 || dirVec.y != 0 ) { double scaling = EuclideanNorm( dirVec ) / aElem.arrowsize; VECTOR2I arrVec = VECTOR2I( KiROUND( dirVec.x / scaling ), KiROUND( dirVec.y / scaling ) ); RotatePoint( arrVec, EDA_ANGLE( 20.0, DEGREES_T ) ); { std::unique_ptr shape1 = std::make_unique( m_board, SHAPE_T::SEGMENT ); shape1->SetLayer( klayer ); shape1->SetStroke( STROKE_PARAMS( aElem.linewidth, LINE_STYLE::SOLID ) ); shape1->SetStart( referencePoint0 ); shape1->SetEnd( referencePoint0 + arrVec ); m_board->Add( shape1.release(), ADD_MODE::APPEND ); } RotatePoint( arrVec, EDA_ANGLE( -40.0, DEGREES_T ) ); { std::unique_ptr shape2 = std::make_unique( m_board, SHAPE_T::SEGMENT ); shape2->SetLayer( klayer ); shape2->SetStroke( STROKE_PARAMS( aElem.linewidth, LINE_STYLE::SOLID ) ); shape2->SetStart( referencePoint0 ); shape2->SetEnd( referencePoint0 + arrVec ); m_board->Add( shape2.release(), ADD_MODE::APPEND ); } } } } if( aElem.textPoint.empty() ) { wxLogError( wxT( "No text position present for leader dimension object" ) ); return; } std::unique_ptr text = std::make_unique( m_board ); text->SetText( aElem.textformat ); text->SetPosition( aElem.textPoint.at( 0 ) ); text->SetLayer( klayer ); text->SetTextSize( VECTOR2I( aElem.textheight, aElem.textheight ) ); // TODO: parse text width text->SetTextThickness( aElem.textlinewidth ); text->SetHorizJustify( GR_TEXT_H_ALIGN_LEFT ); text->SetVertJustify( GR_TEXT_V_ALIGN_BOTTOM ); m_board->Add( text.release(), ADD_MODE::APPEND ); } void ALTIUM_PCB::HelperParseDimensions6Datum( const ADIMENSION6& aElem ) { PCB_LAYER_ID klayer = GetKicadLayer( aElem.layer ); if( klayer == UNDEFINED_LAYER ) { wxLogWarning( _( "Dimension found on an Altium layer (%d) with no KiCad equivalent. " "It has been moved to KiCad layer Eco1_User." ), aElem.layer ); klayer = Eco1_User; } for( size_t i = 0; i < aElem.referencePoint.size(); i++ ) { std::unique_ptr shape = std::make_unique( m_board, SHAPE_T::SEGMENT ); shape->SetLayer( klayer ); shape->SetStroke( STROKE_PARAMS( aElem.linewidth, LINE_STYLE::SOLID ) ); shape->SetStart( aElem.referencePoint.at( i ) ); // shape->SetEnd( /* TODO: seems to be based on TEXTY */ ); m_board->Add( shape.release(), ADD_MODE::APPEND ); } } void ALTIUM_PCB::HelperParseDimensions6Center( const ADIMENSION6& aElem ) { PCB_LAYER_ID klayer = GetKicadLayer( aElem.layer ); if( klayer == UNDEFINED_LAYER ) { wxLogWarning( _( "Dimension found on an Altium layer (%d) with no KiCad equivalent. " "It has been moved to KiCad layer Eco1_User." ), aElem.layer ); klayer = Eco1_User; } VECTOR2I vec = VECTOR2I( 0, aElem.height / 2 ); RotatePoint( vec, EDA_ANGLE( aElem.angle, DEGREES_T ) ); std::unique_ptr dimension = std::make_unique( m_board ); dimension->SetLayer( klayer ); dimension->SetLineThickness( aElem.linewidth ); dimension->SetStart( aElem.xy1 ); dimension->SetEnd( aElem.xy1 + vec ); m_board->Add( dimension.release(), ADD_MODE::APPEND ); } void ALTIUM_PCB::ParseDimensions6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading dimension drawings..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); while( reader.GetRemainingBytes() >= 4 /* TODO: use Header section of file */ ) { checkpoint(); ADIMENSION6 elem( reader ); switch( elem.kind ) { case ALTIUM_DIMENSION_KIND::LINEAR: HelperParseDimensions6Linear( elem ); break; case ALTIUM_DIMENSION_KIND::RADIAL: HelperParseDimensions6Radial( elem ); break; case ALTIUM_DIMENSION_KIND::LEADER: HelperParseDimensions6Leader( elem ); break; case ALTIUM_DIMENSION_KIND::DATUM: wxLogError( _( "Ignored dimension of kind %d (not yet supported)." ), elem.kind ); // HelperParseDimensions6Datum( elem ); break; case ALTIUM_DIMENSION_KIND::CENTER: HelperParseDimensions6Center( elem ); break; default: wxLogError( _( "Ignored dimension of kind %d (not yet supported)." ), elem.kind ); break; } } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( wxT( "Dimensions6 stream is not fully parsed" ) ); } void ALTIUM_PCB::ParseModelsData( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry, const std::vector& aRootDir ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading 3D models..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); if( reader.GetRemainingBytes() == 0 ) return; wxString projectPath = wxPathOnly( m_board->GetFileName() ); // TODO: set KIPRJMOD always after import (not only when loading project)? wxSetEnv( PROJECT_VAR_NAME, projectPath ); // TODO: make this path configurable? const wxString altiumModelDir = wxT( "ALTIUM_EMBEDDED_MODELS" ); wxFileName altiumModelsPath = wxFileName::DirName( projectPath ); wxString kicadModelPrefix = wxT( "${KIPRJMOD}/" ) + altiumModelDir + wxT( "/" ); if( !altiumModelsPath.AppendDir( altiumModelDir ) ) THROW_IO_ERROR( wxT( "Cannot construct directory path for step models" ) ); // Create dir if it does not exist if( !altiumModelsPath.DirExists() ) { if( !altiumModelsPath.Mkdir() ) { wxLogError( _( "Failed to create folder '%s'." ) + wxS( " " ) + _( "No 3D-models will be imported." ), altiumModelsPath.GetFullPath() ); return; } } int idx = 0; wxString invalidChars = wxFileName::GetForbiddenChars(); while( reader.GetRemainingBytes() >= 4 /* TODO: use Header section of file */ ) { checkpoint(); AMODEL elem( reader ); std::vector stepPath = aRootDir; stepPath.emplace_back( std::to_string( idx ) ); bool validName = !elem.name.IsEmpty() && elem.name.IsAscii() && wxString::npos == elem.name.find_first_of( invalidChars ); wxString storageName = !validName ? wxString::Format( wxT( "model_%d" ), idx ) : elem.name; wxFileName storagePath( altiumModelsPath.GetPath(), storageName ); idx++; const CFB::COMPOUND_FILE_ENTRY* stepEntry = aAltiumPcbFile.FindStream( stepPath ); if( stepEntry == nullptr ) { wxLogError( _( "File not found: '%s'. 3D-model not imported." ), FormatPath( stepPath ) ); continue; } size_t stepSize = static_cast( stepEntry->size ); std::vector stepContent( stepSize ); // read file into buffer aAltiumPcbFile.GetCompoundFileReader().ReadFile( stepEntry, 0, stepContent.data(), stepSize ); if( !storagePath.IsDirWritable() ) { wxLogError( _( "Insufficient permissions to save file '%s'." ), storagePath.GetFullPath() ); continue; } wxMemoryInputStream stepStream( stepContent.data(), stepSize ); wxZlibInputStream zlibInputStream( stepStream ); wxFFileOutputStream outputStream( storagePath.GetFullPath() ); if( !outputStream.IsOk() ) { wxLogError( _( "Unable to write file '%s'." ), storagePath.GetFullPath() ); continue; } outputStream.Write( zlibInputStream ); outputStream.Close(); m_models.insert( { elem.id, kicadModelPrefix + storageName } ); } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( wxT( "Models stream is not fully parsed" ) ); } void ALTIUM_PCB::ParseNets6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading nets..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); wxASSERT( m_altiumToKicadNetcodes.empty() ); while( reader.GetRemainingBytes() >= 4 /* TODO: use Header section of file */ ) { checkpoint(); ANET6 elem( reader ); NETINFO_ITEM* netInfo = new NETINFO_ITEM( m_board, elem.name, 0 ); m_board->Add( netInfo, ADD_MODE::APPEND ); // needs to be called after m_board->Add() as assign us the NetCode m_altiumToKicadNetcodes.push_back( netInfo->GetNetCode() ); } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( wxT( "Nets6 stream is not fully parsed" ) ); } void ALTIUM_PCB::ParsePolygons6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading polygons..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); while( reader.GetRemainingBytes() >= 4 /* TODO: use Header section of file */ ) { checkpoint(); APOLYGON6 elem( reader ); SHAPE_LINE_CHAIN linechain; HelperShapeLineChainFromAltiumVertices( linechain, elem.vertices ); if( linechain.PointCount() < 3 ) { // We have found multiple Altium files with polygon records containing nothing but two // coincident vertices. These polygons do not appear when opening the file in Altium. // https://gitlab.com/kicad/code/kicad/-/issues/8183 // Also, polygons with less than 3 points are not supported in KiCad. // // wxLogError( _( "Polygon has only %d point extracted from %ld vertices. At least 2 " // "points are required." ), // linechain.PointCount(), // elem.vertices.size() ); m_polygons.emplace_back( nullptr ); continue; } std::unique_ptr zone = std::make_unique( m_board ); m_polygons.emplace_back( zone.get() ); zone->SetNetCode( GetNetCode( elem.net ) ); zone->SetPosition( elem.vertices.at( 0 ).position ); zone->SetLocked( elem.locked ); zone->SetAssignedPriority( elem.pourindex > 0 ? elem.pourindex : 0 ); zone->Outline()->AddOutline( linechain ); HelperSetZoneLayers( *zone, elem.layer ); if( elem.pourindex > m_highest_pour_index ) m_highest_pour_index = elem.pourindex; const ARULE6* planeClearanceRule = GetRuleDefault( ALTIUM_RULE_KIND::PLANE_CLEARANCE ); const ARULE6* zoneClearanceRule = GetRule( ALTIUM_RULE_KIND::CLEARANCE, wxT( "PolygonClearance" ) ); int planeLayers = 0; int signalLayers = 0; int clearance = 0; for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() ) { LAYER_T layerType = m_board->GetLayerType( layer ); if( layerType == LT_POWER || layerType == LT_MIXED ) planeLayers++; if( layerType == LT_SIGNAL || layerType == LT_MIXED ) signalLayers++; } if( planeLayers > 0 && planeClearanceRule ) clearance = std::max( clearance, planeClearanceRule->planeclearanceClearance ); if( signalLayers > 0 && zoneClearanceRule ) clearance = std::max( clearance, zoneClearanceRule->clearanceGap ); if( clearance > 0 ) zone->SetLocalClearance( clearance ); const ARULE6* polygonConnectRule = GetRuleDefault( ALTIUM_RULE_KIND::POLYGON_CONNECT ); if( polygonConnectRule != nullptr ) { switch( polygonConnectRule->polygonconnectStyle ) { case ALTIUM_CONNECT_STYLE::DIRECT: zone->SetPadConnection( ZONE_CONNECTION::FULL ); break; case ALTIUM_CONNECT_STYLE::NONE: zone->SetPadConnection( ZONE_CONNECTION::NONE ); break; default: case ALTIUM_CONNECT_STYLE::RELIEF: zone->SetPadConnection( ZONE_CONNECTION::THERMAL ); break; } // TODO: correct variables? zone->SetThermalReliefSpokeWidth( polygonConnectRule->polygonconnectReliefconductorwidth ); zone->SetThermalReliefGap( polygonConnectRule->polygonconnectAirgapwidth ); if( polygonConnectRule->polygonconnectReliefconductorwidth < zone->GetMinThickness() ) zone->SetMinThickness( polygonConnectRule->polygonconnectReliefconductorwidth ); } if( IsAltiumLayerAPlane( elem.layer ) ) { // outer zone will be set to priority 0 later. zone->SetAssignedPriority( 1 ); // check if this is the outer zone by simply comparing the BBOX const auto& outer_plane = m_outer_plane.find( elem.layer ); if( outer_plane == m_outer_plane.end() || zone->GetBoundingBox().Contains( outer_plane->second->GetBoundingBox() ) ) { m_outer_plane[elem.layer] = zone.get(); } } if( elem.hatchstyle != ALTIUM_POLYGON_HATCHSTYLE::SOLID && elem.hatchstyle != ALTIUM_POLYGON_HATCHSTYLE::UNKNOWN ) { zone->SetFillMode( ZONE_FILL_MODE::HATCH_PATTERN ); zone->SetHatchThickness( elem.trackwidth ); if( elem.hatchstyle == ALTIUM_POLYGON_HATCHSTYLE::NONE ) { // use a small hack to get us only an outline (hopefully) const BOX2I& bbox = zone->GetBoundingBox(); zone->SetHatchGap( std::max( bbox.GetHeight(), bbox.GetWidth() ) ); } else { zone->SetHatchGap( elem.gridsize - elem.trackwidth ); } if( elem.hatchstyle == ALTIUM_POLYGON_HATCHSTYLE::DEGREE_45 ) zone->SetHatchOrientation( ANGLE_45 ); } zone->SetBorderDisplayStyle( ZONE_BORDER_DISPLAY_STYLE::DIAGONAL_EDGE, ZONE::GetDefaultHatchPitch(), true ); m_board->Add( zone.release(), ADD_MODE::APPEND ); } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( wxT( "Polygons6 stream is not fully parsed" ) ); } void ALTIUM_PCB::ParseRules6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading rules..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); while( reader.GetRemainingBytes() >= 4 /* TODO: use Header section of file */ ) { checkpoint(); ARULE6 elem( reader ); m_rules[elem.kind].emplace_back( elem ); } // sort rules by priority for( std::pair>& val : m_rules ) { std::sort( val.second.begin(), val.second.end(), []( const ARULE6& lhs, const ARULE6& rhs ) { return lhs.priority < rhs.priority; } ); } const ARULE6* clearanceRule = GetRuleDefault( ALTIUM_RULE_KIND::CLEARANCE ); const ARULE6* trackWidthRule = GetRuleDefault( ALTIUM_RULE_KIND::WIDTH ); const ARULE6* routingViasRule = GetRuleDefault( ALTIUM_RULE_KIND::ROUTING_VIAS ); const ARULE6* holeSizeRule = GetRuleDefault( ALTIUM_RULE_KIND::HOLE_SIZE ); const ARULE6* holeToHoleRule = GetRuleDefault( ALTIUM_RULE_KIND::HOLE_TO_HOLE_CLEARANCE ); if( clearanceRule ) m_board->GetDesignSettings().m_MinClearance = clearanceRule->clearanceGap; if( trackWidthRule ) { m_board->GetDesignSettings().m_TrackMinWidth = trackWidthRule->minLimit; // TODO: construct a custom rule for preferredWidth and maxLimit values } if( routingViasRule ) { m_board->GetDesignSettings().m_ViasMinSize = routingViasRule->minWidth; m_board->GetDesignSettings().m_MinThroughDrill = routingViasRule->minHoleWidth; } if( holeSizeRule ) { // TODO: construct a custom rule for minLimit / maxLimit values } if( holeToHoleRule ) m_board->GetDesignSettings().m_HoleToHoleMin = holeToHoleRule->clearanceGap; const ARULE6* soldermaskRule = GetRuleDefault( ALTIUM_RULE_KIND::SOLDER_MASK_EXPANSION ); const ARULE6* pastemaskRule = GetRuleDefault( ALTIUM_RULE_KIND::PASTE_MASK_EXPANSION ); if( soldermaskRule ) m_board->GetDesignSettings().m_SolderMaskExpansion = soldermaskRule->soldermaskExpansion; if( pastemaskRule ) m_board->GetDesignSettings().m_SolderPasteMargin = pastemaskRule->pastemaskExpansion; if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( wxT( "Rules6 stream is not fully parsed" ) ); } void ALTIUM_PCB::ParseBoardRegionsData( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading board regions..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); while( reader.GetRemainingBytes() >= 4 /* TODO: use Header section of file */ ) { checkpoint(); AREGION6 elem( reader, false ); // TODO: implement? } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( wxT( "BoardRegions stream is not fully parsed" ) ); } void ALTIUM_PCB::ParseShapeBasedRegions6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading polygons..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); /* TODO: use Header section of file */ for( int primitiveIndex = 0; reader.GetRemainingBytes() >= 4; primitiveIndex++ ) { checkpoint(); AREGION6 elem( reader, true ); if( elem.component == ALTIUM_COMPONENT_NONE || elem.kind == ALTIUM_REGION_KIND::BOARD_CUTOUT ) { // TODO: implement all different types for footprints ConvertShapeBasedRegions6ToBoardItem( elem ); } else { FOOTPRINT* footprint = HelperGetFootprint( elem.component ); ConvertShapeBasedRegions6ToFootprintItem( footprint, elem, primitiveIndex ); } } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( "ShapeBasedRegions6 stream is not fully parsed" ); } void ALTIUM_PCB::ConvertShapeBasedRegions6ToBoardItem( const AREGION6& aElem ) { if( aElem.kind == ALTIUM_REGION_KIND::BOARD_CUTOUT ) { HelperCreateBoardOutline( aElem.outline ); } else if( aElem.kind == ALTIUM_REGION_KIND::POLYGON_CUTOUT || aElem.is_keepout ) { SHAPE_LINE_CHAIN linechain; HelperShapeLineChainFromAltiumVertices( linechain, aElem.outline ); if( linechain.PointCount() < 3 ) { // We have found multiple Altium files with polygon records containing nothing but // two coincident vertices. These polygons do not appear when opening the file in // Altium. https://gitlab.com/kicad/code/kicad/-/issues/8183 // Also, polygons with less than 3 points are not supported in KiCad. return; } std::unique_ptr zone = std::make_unique( m_board ); zone->SetIsRuleArea( true ); if( aElem.is_keepout ) { HelperSetZoneKeepoutRestrictions( *zone, aElem.keepoutrestrictions ); } else if( aElem.kind == ALTIUM_REGION_KIND::POLYGON_CUTOUT ) { zone->SetDoNotAllowCopperPour( true ); zone->SetDoNotAllowVias( false ); zone->SetDoNotAllowTracks( false ); zone->SetDoNotAllowPads( false ); zone->SetDoNotAllowFootprints( false ); } zone->SetPosition( aElem.outline.at( 0 ).position ); zone->Outline()->AddOutline( linechain ); HelperSetZoneLayers( *zone, aElem.layer ); zone->SetBorderDisplayStyle( ZONE_BORDER_DISPLAY_STYLE::DIAGONAL_EDGE, ZONE::GetDefaultHatchPitch(), true ); m_board->Add( zone.release(), ADD_MODE::APPEND ); } else if( aElem.kind == ALTIUM_REGION_KIND::DASHED_OUTLINE ) { PCB_LAYER_ID klayer = GetKicadLayer( aElem.layer ); if( klayer == UNDEFINED_LAYER ) { wxLogWarning( _( "Dashed outline found on an Altium layer (%d) with no KiCad equivalent. " "It has been moved to KiCad layer Eco1_User." ), aElem.layer ); klayer = Eco1_User; } SHAPE_LINE_CHAIN linechain; HelperShapeLineChainFromAltiumVertices( linechain, aElem.outline ); if( linechain.PointCount() < 3 ) { // We have found multiple Altium files with polygon records containing nothing but // two coincident vertices. These polygons do not appear when opening the file in // Altium. https://gitlab.com/kicad/code/kicad/-/issues/8183 // Also, polygons with less than 3 points are not supported in KiCad. return; } std::unique_ptr shape = std::make_unique( m_board, SHAPE_T::POLY ); shape->SetPolyShape( linechain ); shape->SetFilled( false ); shape->SetLayer( klayer ); shape->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.1 ), LINE_STYLE::DASH ) ); m_board->Add( shape.release(), ADD_MODE::APPEND ); } else if( aElem.kind == ALTIUM_REGION_KIND::COPPER ) { if( aElem.polygon == ALTIUM_POLYGON_NONE ) { for( PCB_LAYER_ID klayer : GetKicadLayersToIterate( aElem.layer ) ) ConvertShapeBasedRegions6ToBoardItemOnLayer( aElem, klayer ); } } else if( aElem.kind == ALTIUM_REGION_KIND::BOARD_CUTOUT ) { ConvertShapeBasedRegions6ToBoardItemOnLayer( aElem, Edge_Cuts ); } else { wxLogError( _( "Ignored polygon shape of kind %d (not yet supported)." ), aElem.kind ); } } void ALTIUM_PCB::ConvertShapeBasedRegions6ToFootprintItem( FOOTPRINT* aFootprint, const AREGION6& aElem, const int aPrimitiveIndex ) { if( aElem.kind == ALTIUM_REGION_KIND::POLYGON_CUTOUT || aElem.is_keepout ) { SHAPE_LINE_CHAIN linechain; HelperShapeLineChainFromAltiumVertices( linechain, aElem.outline ); if( linechain.PointCount() < 3 ) { // We have found multiple Altium files with polygon records containing nothing but // two coincident vertices. These polygons do not appear when opening the file in // Altium. https://gitlab.com/kicad/code/kicad/-/issues/8183 // Also, polygons with less than 3 points are not supported in KiCad. return; } std::unique_ptr zone = std::make_unique( aFootprint ); zone->SetIsRuleArea( true ); if( aElem.is_keepout ) { HelperSetZoneKeepoutRestrictions( *zone, aElem.keepoutrestrictions ); } else if( aElem.kind == ALTIUM_REGION_KIND::POLYGON_CUTOUT ) { zone->SetDoNotAllowCopperPour( true ); zone->SetDoNotAllowVias( false ); zone->SetDoNotAllowTracks( false ); zone->SetDoNotAllowPads( false ); zone->SetDoNotAllowFootprints( false ); } zone->SetPosition( aElem.outline.at( 0 ).position ); zone->Outline()->AddOutline( linechain ); HelperSetZoneLayers( *zone, aElem.layer ); zone->SetBorderDisplayStyle( ZONE_BORDER_DISPLAY_STYLE::DIAGONAL_EDGE, ZONE::GetDefaultHatchPitch(), true ); aFootprint->Add( zone.release(), ADD_MODE::APPEND ); } else if( aElem.kind == ALTIUM_REGION_KIND::COPPER ) { if( aElem.polygon == ALTIUM_POLYGON_NONE ) { for( PCB_LAYER_ID klayer : GetKicadLayersToIterate( aElem.layer ) ) { ConvertShapeBasedRegions6ToFootprintItemOnLayer( aFootprint, aElem, klayer, aPrimitiveIndex ); } } } else if( aElem.kind == ALTIUM_REGION_KIND::DASHED_OUTLINE || aElem.kind == ALTIUM_REGION_KIND::BOARD_CUTOUT ) { PCB_LAYER_ID klayer = aElem.kind == ALTIUM_REGION_KIND::BOARD_CUTOUT ? Edge_Cuts : GetKicadLayer( aElem.layer ); if( klayer == UNDEFINED_LAYER ) { if( !m_footprintName.IsEmpty() ) { wxLogWarning( _( "Loading library '%s':\n" "Footprint %s contains a dashed outline on Altium layer (%d) with " "no KiCad equivalent. It has been moved to KiCad layer Eco1_User." ), m_library, m_footprintName, aElem.layer ); } else { wxLogWarning( _( "Footprint %s contains a dashed outline on Altium layer (%d) with " "no KiCad equivalent. It has been moved to KiCad layer Eco1_User." ), aFootprint->GetReference(), aElem.layer ); } klayer = Eco1_User; } SHAPE_LINE_CHAIN linechain; HelperShapeLineChainFromAltiumVertices( linechain, aElem.outline ); if( linechain.PointCount() < 3 ) { // We have found multiple Altium files with polygon records containing nothing but // two coincident vertices. These polygons do not appear when opening the file in // Altium. https://gitlab.com/kicad/code/kicad/-/issues/8183 // Also, polygons with less than 3 points are not supported in KiCad. return; } std::unique_ptr shape = std::make_unique( aFootprint, SHAPE_T::POLY ); shape->SetPolyShape( linechain ); shape->SetFilled( false ); shape->SetLayer( klayer ); if( aElem.kind == ALTIUM_REGION_KIND::DASHED_OUTLINE ) shape->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.1 ), LINE_STYLE::DASH ) ); else shape->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.1 ), LINE_STYLE::SOLID ) ); aFootprint->Add( shape.release(), ADD_MODE::APPEND ); } else { if( !m_footprintName.IsEmpty() ) { wxLogError( _( "Error loading library '%s':\n" "Footprint %s contains polygon shape of kind %d (not yet supported)." ), m_library, m_footprintName, aElem.kind ); } else { wxLogError( _( "Footprint %s contains polygon shape of kind %d (not yet supported)." ), aFootprint->GetReference(), aElem.kind ); } } } void ALTIUM_PCB::ConvertShapeBasedRegions6ToBoardItemOnLayer( const AREGION6& aElem, PCB_LAYER_ID aLayer ) { SHAPE_LINE_CHAIN linechain; HelperShapeLineChainFromAltiumVertices( linechain, aElem.outline ); if( linechain.PointCount() < 3 ) { // We have found multiple Altium files with polygon records containing nothing // but two coincident vertices. These polygons do not appear when opening the // file in Altium. https://gitlab.com/kicad/code/kicad/-/issues/8183 // Also, polygons with less than 3 points are not supported in KiCad. return; } SHAPE_POLY_SET polySet; polySet.AddOutline( linechain ); for( const std::vector& hole : aElem.holes ) { SHAPE_LINE_CHAIN hole_linechain; HelperShapeLineChainFromAltiumVertices( hole_linechain, hole ); if( hole_linechain.PointCount() < 3 ) continue; polySet.AddHole( hole_linechain ); } std::unique_ptr shape = std::make_unique( m_board, SHAPE_T::POLY ); shape->SetPolyShape( polySet ); shape->SetFilled( true ); shape->SetLayer( aLayer ); shape->SetStroke( STROKE_PARAMS( 0 ) ); if( IsCopperLayer( aLayer ) && aElem.net != ALTIUM_NET_UNCONNECTED ) { shape->SetNetCode( GetNetCode( aElem.net ) ); } m_board->Add( shape.release(), ADD_MODE::APPEND ); } void ALTIUM_PCB::ConvertShapeBasedRegions6ToFootprintItemOnLayer( FOOTPRINT* aFootprint, const AREGION6& aElem, PCB_LAYER_ID aLayer, const int aPrimitiveIndex ) { SHAPE_LINE_CHAIN linechain; HelperShapeLineChainFromAltiumVertices( linechain, aElem.outline ); if( linechain.PointCount() < 3 ) { // We have found multiple Altium files with polygon records containing nothing // but two coincident vertices. These polygons do not appear when opening the // file in Altium. https://gitlab.com/kicad/code/kicad/-/issues/8183 // Also, polygons with less than 3 points are not supported in KiCad. return; } SHAPE_POLY_SET polySet; polySet.AddOutline( linechain ); for( const std::vector& hole : aElem.holes ) { SHAPE_LINE_CHAIN hole_linechain; HelperShapeLineChainFromAltiumVertices( hole_linechain, hole ); if( hole_linechain.PointCount() < 3 ) continue; polySet.AddHole( hole_linechain ); } if( aLayer == F_Cu || aLayer == B_Cu ) { std::unique_ptr pad = std::make_unique( aFootprint ); LSET padLayers; padLayers.set( aLayer ); pad->SetKeepTopBottom( false ); // TODO: correct? This seems to be KiCad default on import pad->SetAttribute( PAD_ATTRIB::SMD ); pad->SetShape( PAD_SHAPE::CUSTOM ); int anchorSize = 1; VECTOR2I anchorPos = linechain.CPoint( 0 ); pad->SetShape( PAD_SHAPE::CUSTOM ); pad->SetAnchorPadShape( PAD_SHAPE::CIRCLE ); pad->SetSize( { anchorSize, anchorSize } ); pad->SetPosition( anchorPos ); SHAPE_POLY_SET shapePolys = polySet; shapePolys.Move( -anchorPos ); pad->AddPrimitivePoly( shapePolys, 0, true ); auto& map = m_extendedPrimitiveInformationMaps[ALTIUM_RECORD::REGION]; auto it = map.find( aPrimitiveIndex ); if( it != map.end() ) { const AEXTENDED_PRIMITIVE_INFORMATION& info = it->second; if( info.pastemaskexpansionmode == ALTIUM_MODE::MANUAL ) { pad->SetLocalSolderPasteMargin( info.pastemaskexpansionmanual ? info.pastemaskexpansionmanual : 1 ); } if( info.soldermaskexpansionmode == ALTIUM_MODE::MANUAL ) { pad->SetLocalSolderMaskMargin( info.soldermaskexpansionmanual ? info.soldermaskexpansionmanual : 1 ); } if( info.pastemaskexpansionmode != ALTIUM_MODE::NONE ) padLayers.set( aLayer == F_Cu ? F_Paste : B_Paste ); if( info.soldermaskexpansionmode != ALTIUM_MODE::NONE ) padLayers.set( aLayer == F_Cu ? F_Mask : B_Mask ); } pad->SetLayerSet( padLayers ); aFootprint->Add( pad.release(), ADD_MODE::APPEND ); } else { std::unique_ptr shape = std::make_unique( aFootprint, SHAPE_T::POLY ); shape->SetPolyShape( polySet ); shape->SetFilled( true ); shape->SetLayer( aLayer ); shape->SetStroke( STROKE_PARAMS( 0 ) ); aFootprint->Add( shape.release(), ADD_MODE::APPEND ); } } void ALTIUM_PCB::ParseRegions6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading zone fills..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); while( reader.GetRemainingBytes() >= 4 /* TODO: use Header section of file */ ) { checkpoint(); AREGION6 elem( reader, false ); if( elem.polygon != ALTIUM_POLYGON_NONE ) { if( m_polygons.size() <= elem.polygon ) { THROW_IO_ERROR( wxString::Format( "Region stream tries to access polygon id %d " "of %d existing polygons.", elem.polygon, m_polygons.size() ) ); } ZONE* zone = m_polygons.at( elem.polygon ); if( zone == nullptr ) { continue; // we know the zone id, but because we do not know the layer we did not // add it! } PCB_LAYER_ID klayer = GetKicadLayer( elem.layer ); if( klayer == UNDEFINED_LAYER ) continue; // Just skip it for now. Users can fill it themselves. SHAPE_LINE_CHAIN linechain; for( const ALTIUM_VERTICE& vertice : elem.outline ) linechain.Append( vertice.position ); linechain.Append( elem.outline.at( 0 ).position ); linechain.SetClosed( true ); SHAPE_POLY_SET fill; fill.AddOutline( linechain ); for( const std::vector& hole : elem.holes ) { SHAPE_LINE_CHAIN hole_linechain; for( const ALTIUM_VERTICE& vertice : hole ) hole_linechain.Append( vertice.position ); hole_linechain.Append( hole.at( 0 ).position ); hole_linechain.SetClosed( true ); fill.AddHole( hole_linechain ); } if( zone->HasFilledPolysForLayer( klayer ) ) fill.BooleanAdd( *zone->GetFill( klayer ), SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); fill.Fracture( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); zone->SetFilledPolysList( klayer, fill ); zone->SetIsFilled( true ); zone->SetNeedRefill( false ); } } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( wxT( "Regions6 stream is not fully parsed" ) ); } void ALTIUM_PCB::ParseArcs6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading arcs..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); for( int primitiveIndex = 0; reader.GetRemainingBytes() >= 4; primitiveIndex++ ) { checkpoint(); AARC6 elem( reader ); if( elem.component == ALTIUM_COMPONENT_NONE ) { ConvertArcs6ToBoardItem( elem, primitiveIndex ); } else { FOOTPRINT* footprint = HelperGetFootprint( elem.component ); ConvertArcs6ToFootprintItem( footprint, elem, primitiveIndex, true ); } } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( "Arcs6 stream is not fully parsed" ); } void ALTIUM_PCB::ConvertArcs6ToPcbShape( const AARC6& aElem, PCB_SHAPE* aShape ) { if( aElem.startangle == 0. && aElem.endangle == 360. ) { aShape->SetShape( SHAPE_T::CIRCLE ); // TODO: other variants to define circle? aShape->SetStart( aElem.center ); aShape->SetEnd( aElem.center - VECTOR2I( 0, aElem.radius ) ); } else { aShape->SetShape( SHAPE_T::ARC ); EDA_ANGLE includedAngle( aElem.endangle - aElem.startangle, DEGREES_T ); EDA_ANGLE startAngle( aElem.endangle, DEGREES_T ); VECTOR2I startOffset = VECTOR2I( KiROUND( startAngle.Cos() * aElem.radius ), -KiROUND( startAngle.Sin() * aElem.radius ) ); aShape->SetCenter( aElem.center ); aShape->SetStart( aElem.center + startOffset ); aShape->SetArcAngleAndEnd( includedAngle.Normalize(), true ); } } void ALTIUM_PCB::ConvertArcs6ToBoardItem( const AARC6& aElem, const int aPrimitiveIndex ) { if( aElem.polygon != ALTIUM_POLYGON_NONE && aElem.polygon != ALTIUM_POLYGON_BOARD ) { if( m_polygons.size() <= aElem.polygon ) { THROW_IO_ERROR( wxString::Format( "Tracks stream tries to access polygon id %u " "of %zu existing polygons.", aElem.polygon, m_polygons.size() ) ); } ZONE* zone = m_polygons.at( aElem.polygon ); if( zone == nullptr ) { return; // we know the zone id, but because we do not know the layer we did not // add it! } PCB_LAYER_ID klayer = GetKicadLayer( aElem.layer ); if( klayer == UNDEFINED_LAYER ) return; // Just skip it for now. Users can fill it themselves. SHAPE_POLY_SET* fill = zone->GetFill( klayer ); // This is not the actual board item. We can use it to create the polygon for the region PCB_SHAPE shape( nullptr ); ConvertArcs6ToPcbShape( aElem, &shape ); shape.SetStroke( STROKE_PARAMS( aElem.width, LINE_STYLE::SOLID ) ); shape.EDA_SHAPE::TransformShapeToPolygon( *fill, 0, ARC_HIGH_DEF, ERROR_INSIDE ); // Will be simplified and fractured later zone->SetIsFilled( true ); zone->SetNeedRefill( false ); return; } if( aElem.is_keepout || aElem.layer == ALTIUM_LAYER::KEEP_OUT_LAYER || IsAltiumLayerAPlane( aElem.layer ) ) { // This is not the actual board item. We can use it to create the polygon for the region PCB_SHAPE shape( nullptr ); ConvertArcs6ToPcbShape( aElem, &shape ); shape.SetStroke( STROKE_PARAMS( aElem.width, LINE_STYLE::SOLID ) ); HelperPcpShapeAsBoardKeepoutRegion( shape, aElem.layer, aElem.keepoutrestrictions ); } else { for( PCB_LAYER_ID klayer : GetKicadLayersToIterate( aElem.layer ) ) ConvertArcs6ToBoardItemOnLayer( aElem, klayer ); } for( const auto& layerExpansionMask : HelperGetSolderAndPasteMaskExpansions( ALTIUM_RECORD::ARC, aPrimitiveIndex, aElem.layer ) ) { int width = aElem.width + ( layerExpansionMask.second * 2 ); if( width > 1 ) { std::unique_ptr arc = std::make_unique( m_board ); ConvertArcs6ToPcbShape( aElem, arc.get() ); arc->SetStroke( STROKE_PARAMS( width, LINE_STYLE::SOLID ) ); arc->SetLayer( layerExpansionMask.first ); m_board->Add( arc.release(), ADD_MODE::APPEND ); } } } void ALTIUM_PCB::ConvertArcs6ToFootprintItem( FOOTPRINT* aFootprint, const AARC6& aElem, const int aPrimitiveIndex, const bool aIsBoardImport ) { if( aElem.polygon != ALTIUM_POLYGON_NONE ) { wxFAIL_MSG( wxString::Format( "Altium: Unexpected footprint Arc with polygon id %d", aElem.polygon ) ); return; } if( aElem.is_keepout || aElem.layer == ALTIUM_LAYER::KEEP_OUT_LAYER || IsAltiumLayerAPlane( aElem.layer ) ) { // This is not the actual board item. We can use it to create the polygon for the region PCB_SHAPE shape( nullptr ); ConvertArcs6ToPcbShape( aElem, &shape ); shape.SetStroke( STROKE_PARAMS( aElem.width, LINE_STYLE::SOLID ) ); HelperPcpShapeAsFootprintKeepoutRegion( aFootprint, shape, aElem.layer, aElem.keepoutrestrictions ); } else { for( PCB_LAYER_ID klayer : GetKicadLayersToIterate( aElem.layer ) ) { if( aIsBoardImport && IsCopperLayer( klayer ) && aElem.net != ALTIUM_NET_UNCONNECTED ) { // Special case: do to not lose net connections in footprints ConvertArcs6ToBoardItemOnLayer( aElem, klayer ); } else { ConvertArcs6ToFootprintItemOnLayer( aFootprint, aElem, klayer ); } } } for( const auto& layerExpansionMask : HelperGetSolderAndPasteMaskExpansions( ALTIUM_RECORD::ARC, aPrimitiveIndex, aElem.layer ) ) { int width = aElem.width + ( layerExpansionMask.second * 2 ); if( width > 1 ) { std::unique_ptr arc = std::make_unique( aFootprint ); ConvertArcs6ToPcbShape( aElem, arc.get() ); arc->SetStroke( STROKE_PARAMS( width, LINE_STYLE::SOLID ) ); arc->SetLayer( layerExpansionMask.first ); aFootprint->Add( arc.release(), ADD_MODE::APPEND ); } } } void ALTIUM_PCB::ConvertArcs6ToBoardItemOnLayer( const AARC6& aElem, PCB_LAYER_ID aLayer ) { if( IsCopperLayer( aLayer ) && aElem.net != ALTIUM_NET_UNCONNECTED ) { // TODO: This is not the actual board item. We use it for now to calculate the arc points. This could be improved! PCB_SHAPE shape( nullptr, SHAPE_T::ARC ); EDA_ANGLE includedAngle( aElem.endangle - aElem.startangle, DEGREES_T ); EDA_ANGLE startAngle( aElem.endangle, DEGREES_T ); VECTOR2I startOffset = VECTOR2I( KiROUND( startAngle.Cos() * aElem.radius ), -KiROUND( startAngle.Sin() * aElem.radius ) ); shape.SetCenter( aElem.center ); shape.SetStart( aElem.center + startOffset ); shape.SetArcAngleAndEnd( includedAngle.Normalize(), true ); // Create actual arc SHAPE_ARC shapeArc( shape.GetCenter(), shape.GetStart(), shape.GetArcAngle(), aElem.width ); std::unique_ptr arc = std::make_unique( m_board, &shapeArc ); arc->SetWidth( aElem.width ); arc->SetLayer( aLayer ); arc->SetNetCode( GetNetCode( aElem.net ) ); m_board->Add( arc.release(), ADD_MODE::APPEND ); } else { std::unique_ptr arc = std::make_unique( m_board ); ConvertArcs6ToPcbShape( aElem, arc.get() ); arc->SetStroke( STROKE_PARAMS( aElem.width, LINE_STYLE::SOLID ) ); arc->SetLayer( aLayer ); m_board->Add( arc.release(), ADD_MODE::APPEND ); } } void ALTIUM_PCB::ConvertArcs6ToFootprintItemOnLayer( FOOTPRINT* aFootprint, const AARC6& aElem, PCB_LAYER_ID aLayer ) { std::unique_ptr arc = std::make_unique( aFootprint ); ConvertArcs6ToPcbShape( aElem, arc.get() ); arc->SetStroke( STROKE_PARAMS( aElem.width, LINE_STYLE::SOLID ) ); arc->SetLayer( aLayer ); aFootprint->Add( arc.release(), ADD_MODE::APPEND ); } void ALTIUM_PCB::ParsePads6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading pads..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); while( reader.GetRemainingBytes() >= 4 /* TODO: use Header section of file */ ) { checkpoint(); APAD6 elem( reader ); if( elem.component == ALTIUM_COMPONENT_NONE ) { ConvertPads6ToBoardItem( elem ); } else { FOOTPRINT* footprint = HelperGetFootprint( elem.component ); ConvertPads6ToFootprintItem( footprint, elem ); } } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( wxT( "Pads6 stream is not fully parsed" ) ); } void ALTIUM_PCB::ConvertPads6ToBoardItem( const APAD6& aElem ) { // It is possible to place altium pads on non-copper layers -> we need to interpolate them using drawings! if( !IsAltiumLayerCopper( aElem.layer ) && !IsAltiumLayerAPlane( aElem.layer ) && aElem.layer != ALTIUM_LAYER::MULTI_LAYER ) { ConvertPads6ToBoardItemOnNonCopper( aElem ); } else { // We cannot add a pad directly into the PCB std::unique_ptr footprint = std::make_unique( m_board ); footprint->SetPosition( aElem.position ); ConvertPads6ToFootprintItemOnCopper( footprint.get(), aElem ); m_board->Add( footprint.release(), ADD_MODE::APPEND ); } } void ALTIUM_PCB::ConvertPads6ToFootprintItem( FOOTPRINT* aFootprint, const APAD6& aElem ) { // It is possible to place altium pads on non-copper layers -> we need to interpolate them using drawings! if( !IsAltiumLayerCopper( aElem.layer ) && !IsAltiumLayerAPlane( aElem.layer ) && aElem.layer != ALTIUM_LAYER::MULTI_LAYER ) { ConvertPads6ToFootprintItemOnNonCopper( aFootprint, aElem ); } else { ConvertPads6ToFootprintItemOnCopper( aFootprint, aElem ); } } void ALTIUM_PCB::ConvertPads6ToFootprintItemOnCopper( FOOTPRINT* aFootprint, const APAD6& aElem ) { std::unique_ptr pad = std::make_unique( aFootprint ); pad->SetKeepTopBottom( false ); // TODO: correct? This seems to be KiCad default on import pad->SetNumber( aElem.name ); pad->SetNetCode( GetNetCode( aElem.net ) ); pad->SetPosition( aElem.position ); pad->SetOrientationDegrees( aElem.direction ); pad->SetSize( aElem.topsize ); if( aElem.holesize == 0 ) { pad->SetAttribute( PAD_ATTRIB::SMD ); } else { if( aElem.layer != ALTIUM_LAYER::MULTI_LAYER ) { // TODO: I assume other values are possible as well? if( !m_footprintName.IsEmpty() ) { wxLogError( _( "Error loading library '%s':\n" "Footprint %s pad %s is not marked as multilayer, but is a TH pad." ), m_library, m_footprintName, aElem.name ); } else { wxLogError( _( "Footprint %s pad %s is not marked as multilayer, but is a TH pad." ), aFootprint->GetReference(), aElem.name ); } } pad->SetAttribute( aElem.plated ? PAD_ATTRIB::PTH : PAD_ATTRIB::NPTH ); if( !aElem.sizeAndShape || aElem.sizeAndShape->holeshape == ALTIUM_PAD_HOLE_SHAPE::ROUND ) { pad->SetDrillShape( PAD_DRILL_SHAPE_T::PAD_DRILL_SHAPE_CIRCLE ); pad->SetDrillSize( VECTOR2I( aElem.holesize, aElem.holesize ) ); } else { switch( aElem.sizeAndShape->holeshape ) { case ALTIUM_PAD_HOLE_SHAPE::ROUND: wxFAIL_MSG( wxT( "Round holes are handled before the switch" ) ); break; case ALTIUM_PAD_HOLE_SHAPE::SQUARE: if( !m_footprintName.IsEmpty() ) { wxLogWarning( _( "Loading library '%s':\n" "Footprint %s pad %s has a square hole (not yet supported)." ), m_library, m_footprintName, aElem.name ); } else { wxLogWarning( _( "Footprint %s pad %s has a square hole (not yet supported)." ), aFootprint->GetReference(), aElem.name ); } pad->SetDrillShape( PAD_DRILL_SHAPE_T::PAD_DRILL_SHAPE_CIRCLE ); pad->SetDrillSize( VECTOR2I( aElem.holesize, aElem.holesize ) ); // Workaround // TODO: elem.sizeAndShape->slotsize was 0 in testfile. Either use holesize in // this case or rect holes have a different id break; case ALTIUM_PAD_HOLE_SHAPE::SLOT: { pad->SetDrillShape( PAD_DRILL_SHAPE_T::PAD_DRILL_SHAPE_OBLONG ); EDA_ANGLE slotRotation( aElem.sizeAndShape->slotrotation, DEGREES_T ); slotRotation.Normalize(); if( slotRotation.IsHorizontal() ) { pad->SetDrillSize( VECTOR2I( aElem.sizeAndShape->slotsize, aElem.holesize ) ); } else if( slotRotation.IsVertical() ) { pad->SetDrillSize( VECTOR2I( aElem.holesize, aElem.sizeAndShape->slotsize ) ); } else { if( !m_footprintName.IsEmpty() ) { wxLogWarning( _( "Loading library '%s':\n" "Footprint %s pad %s has a hole-rotation of %f degrees. " "KiCad only supports 90 degree rotations." ), m_library, m_footprintName, aElem.name, slotRotation.AsDegrees() ); } else { wxLogWarning( _( "Footprint %s pad %s has a hole-rotation of %f degrees. " "KiCad only supports 90 degree rotations." ), aFootprint->GetReference(), aElem.name, slotRotation.AsDegrees() ); } } break; } default: case ALTIUM_PAD_HOLE_SHAPE::UNKNOWN: if( !m_footprintName.IsEmpty() ) { wxLogError( _( "Error loading library '%s':\n" "Footprint %s pad %s uses a hole of unknown kind %d." ), m_library, m_footprintName, aElem.name, aElem.sizeAndShape->holeshape ); } else { wxLogError( _( "Footprint %s pad %s uses a hole of unknown kind %d." ), aFootprint->GetReference(), aElem.name, aElem.sizeAndShape->holeshape ); } pad->SetDrillShape( PAD_DRILL_SHAPE_T::PAD_DRILL_SHAPE_CIRCLE ); pad->SetDrillSize( VECTOR2I( aElem.holesize, aElem.holesize ) ); // Workaround break; } } if( aElem.sizeAndShape ) pad->SetOffset( aElem.sizeAndShape->holeoffset[0] ); } if( aElem.padmode != ALTIUM_PAD_MODE::SIMPLE ) { if( !m_footprintName.IsEmpty() ) { wxLogError( _( "Error loading library '%s':\n" "Footprint %s pad %s uses a complex pad stack (not yet supported)." ), m_library, m_footprintName, aElem.name ); } else { wxLogError( _( "Footprint %s pad %s uses a complex pad stack (not yet supported)." ), aFootprint->GetReference(), aElem.name ); } } switch( aElem.topshape ) { case ALTIUM_PAD_SHAPE::RECT: pad->SetShape( PAD_SHAPE::RECTANGLE ); break; case ALTIUM_PAD_SHAPE::CIRCLE: if( aElem.sizeAndShape && aElem.sizeAndShape->alt_shape[0] == ALTIUM_PAD_SHAPE_ALT::ROUNDRECT ) { pad->SetShape( PAD_SHAPE::ROUNDRECT ); // 100 = round, 0 = rectangular double ratio = aElem.sizeAndShape->cornerradius[0] / 200.; pad->SetRoundRectRadiusRatio( ratio ); } else if( aElem.topsize.x == aElem.topsize.y ) { pad->SetShape( PAD_SHAPE::CIRCLE ); } else { pad->SetShape( PAD_SHAPE::OVAL ); } break; case ALTIUM_PAD_SHAPE::OCTAGONAL: pad->SetShape( PAD_SHAPE::CHAMFERED_RECT ); pad->SetChamferPositions( RECT_CHAMFER_ALL ); pad->SetChamferRectRatio( 0.25 ); break; case ALTIUM_PAD_SHAPE::UNKNOWN: default: if( !m_footprintName.IsEmpty() ) { wxLogError( _( "Error loading library '%s':\n" "Footprint %s pad %s uses an unknown pad-shape." ), m_library, m_footprintName, aElem.name ); } else { wxLogError( _( "Footprint %s pad %s uses an unknown pad-shape." ), aFootprint->GetReference(), aElem.name ); } break; } if( pad->GetAttribute() == PAD_ATTRIB::NPTH && pad->HasHole() ) { // KiCad likes NPTH pads to be the same size & shape as their holes pad->SetShape( pad->GetDrillShape() == PAD_DRILL_SHAPE_CIRCLE ? PAD_SHAPE::CIRCLE : PAD_SHAPE::OVAL ); pad->SetSize( pad->GetDrillSize() ); } switch( aElem.layer ) { case ALTIUM_LAYER::TOP_LAYER: pad->SetLayer( F_Cu ); pad->SetLayerSet( PAD::SMDMask() ); break; case ALTIUM_LAYER::BOTTOM_LAYER: pad->SetLayer( B_Cu ); pad->SetLayerSet( FlipLayerMask( PAD::SMDMask() ) ); break; case ALTIUM_LAYER::MULTI_LAYER: pad->SetLayerSet( aElem.plated ? PAD::PTHMask() : PAD::UnplatedHoleMask() ); break; default: PCB_LAYER_ID klayer = GetKicadLayer( aElem.layer ); pad->SetLayer( klayer ); pad->SetLayerSet( LSET( 1, klayer ) ); break; } if( aElem.pastemaskexpansionmode == ALTIUM_MODE::MANUAL ) pad->SetLocalSolderPasteMargin( aElem.pastemaskexpansionmanual ); if( aElem.soldermaskexpansionmode == ALTIUM_MODE::MANUAL ) pad->SetLocalSolderMaskMargin( aElem.soldermaskexpansionmanual ); if( aElem.is_tent_top ) pad->SetLayerSet( pad->GetLayerSet().reset( F_Mask ) ); if( aElem.is_tent_bottom ) pad->SetLayerSet( pad->GetLayerSet().reset( B_Mask ) ); aFootprint->Add( pad.release(), ADD_MODE::APPEND ); } void ALTIUM_PCB::ConvertPads6ToBoardItemOnNonCopper( const APAD6& aElem ) { PCB_LAYER_ID klayer = GetKicadLayer( aElem.layer ); if( klayer == UNDEFINED_LAYER ) { wxLogWarning( _( "Non-copper pad %s found on an Altium layer (%d) with no KiCad " "equivalent. It has been moved to KiCad layer Eco1_User." ), aElem.name, aElem.layer ); klayer = Eco1_User; } std::unique_ptr pad = std::make_unique( m_board ); HelperParsePad6NonCopper( aElem, klayer, pad.get() ); m_board->Add( pad.release(), ADD_MODE::APPEND ); } void ALTIUM_PCB::ConvertPads6ToFootprintItemOnNonCopper( FOOTPRINT* aFootprint, const APAD6& aElem ) { PCB_LAYER_ID klayer = GetKicadLayer( aElem.layer ); if( klayer == UNDEFINED_LAYER ) { if( !m_footprintName.IsEmpty() ) { wxLogWarning( _( "Loading library '%s':\n" "Footprint %s non-copper pad %s found on an Altium layer (%d) with no " "KiCad equivalent. It has been moved to KiCad layer Eco1_User." ), m_library, m_footprintName, aElem.name, aElem.layer ); } else { wxLogWarning( _( "Footprint %s non-copper pad %s found on an Altium layer (%d) with no " "KiCad equivalent. It has been moved to KiCad layer Eco1_User." ), aFootprint->GetReference(), aElem.name, aElem.layer ); } klayer = Eco1_User; } std::unique_ptr pad = std::make_unique( aFootprint ); HelperParsePad6NonCopper( aElem, klayer, pad.get() ); aFootprint->Add( pad.release(), ADD_MODE::APPEND ); } void ALTIUM_PCB::HelperParsePad6NonCopper( const APAD6& aElem, PCB_LAYER_ID aLayer, PCB_SHAPE* aShape ) { if( aElem.net != ALTIUM_NET_UNCONNECTED ) { wxLogError( _( "Non-copper pad %s is connected to a net, which is not supported." ), aElem.name ); } if( aElem.holesize != 0 ) { wxLogError( _( "Non-copper pad %s has a hole, which is not supported." ), aElem.name ); } if( aElem.padmode != ALTIUM_PAD_MODE::SIMPLE ) { wxLogWarning( _( "Non-copper pad %s has a complex pad stack (not yet supported)." ), aElem.name ); } switch( aElem.topshape ) { case ALTIUM_PAD_SHAPE::RECT: { // filled rect aShape->SetShape( SHAPE_T::POLY ); aShape->SetFilled( true ); aShape->SetLayer( aLayer ); aShape->SetStroke( STROKE_PARAMS( 0 ) ); aShape->SetPolyPoints( { aElem.position + VECTOR2I( aElem.topsize.x / 2, aElem.topsize.y / 2 ), aElem.position + VECTOR2I( aElem.topsize.x / 2, -aElem.topsize.y / 2 ), aElem.position + VECTOR2I( -aElem.topsize.x / 2, -aElem.topsize.y / 2 ), aElem.position + VECTOR2I( -aElem.topsize.x / 2, aElem.topsize.y / 2 ) } ); if( aElem.direction != 0 ) aShape->Rotate( aElem.position, EDA_ANGLE( aElem.direction, DEGREES_T ) ); } break; case ALTIUM_PAD_SHAPE::CIRCLE: if( aElem.sizeAndShape && aElem.sizeAndShape->alt_shape[0] == ALTIUM_PAD_SHAPE_ALT::ROUNDRECT ) { // filled roundrect int cornerradius = aElem.sizeAndShape->cornerradius[0]; int offset = ( std::min( aElem.topsize.x, aElem.topsize.y ) * cornerradius ) / 200; aShape->SetLayer( aLayer ); aShape->SetStroke( STROKE_PARAMS( offset * 2, LINE_STYLE::SOLID ) ); if( cornerradius < 100 ) { int offsetX = aElem.topsize.x / 2 - offset; int offsetY = aElem.topsize.y / 2 - offset; VECTOR2I p11 = aElem.position + VECTOR2I( offsetX, offsetY ); VECTOR2I p12 = aElem.position + VECTOR2I( offsetX, -offsetY ); VECTOR2I p22 = aElem.position + VECTOR2I( -offsetX, -offsetY ); VECTOR2I p21 = aElem.position + VECTOR2I( -offsetX, offsetY ); aShape->SetShape( SHAPE_T::POLY ); aShape->SetFilled( true ); aShape->SetPolyPoints( { p11, p12, p22, p21 } ); } else if( aElem.topsize.x == aElem.topsize.y ) { // circle aShape->SetShape( SHAPE_T::CIRCLE ); aShape->SetFilled( true ); aShape->SetStart( aElem.position ); aShape->SetEnd( aElem.position - VECTOR2I( 0, aElem.topsize.x / 4 ) ); aShape->SetStroke( STROKE_PARAMS( aElem.topsize.x / 2, LINE_STYLE::SOLID ) ); } else if( aElem.topsize.x < aElem.topsize.y ) { // short vertical line aShape->SetShape( SHAPE_T::SEGMENT ); VECTOR2I pointOffset( 0, ( aElem.topsize.y - aElem.topsize.x ) / 2 ); aShape->SetStart( aElem.position + pointOffset ); aShape->SetEnd( aElem.position - pointOffset ); } else { // short horizontal line aShape->SetShape( SHAPE_T::SEGMENT ); VECTOR2I pointOffset( ( aElem.topsize.x - aElem.topsize.y ) / 2, 0 ); aShape->SetStart( aElem.position + pointOffset ); aShape->SetEnd( aElem.position - pointOffset ); } if( aElem.direction != 0 ) aShape->Rotate( aElem.position, EDA_ANGLE( aElem.direction, DEGREES_T ) ); } else if( aElem.topsize.x == aElem.topsize.y ) { // filled circle aShape->SetShape( SHAPE_T::CIRCLE ); aShape->SetFilled( true ); aShape->SetLayer( aLayer ); aShape->SetStart( aElem.position ); aShape->SetEnd( aElem.position - VECTOR2I( 0, aElem.topsize.x / 4 ) ); aShape->SetStroke( STROKE_PARAMS( aElem.topsize.x / 2, LINE_STYLE::SOLID ) ); } else { // short line aShape->SetShape( SHAPE_T::SEGMENT ); aShape->SetLayer( aLayer ); aShape->SetStroke( STROKE_PARAMS( std::min( aElem.topsize.x, aElem.topsize.y ), LINE_STYLE::SOLID ) ); if( aElem.topsize.x < aElem.topsize.y ) { VECTOR2I offset( 0, ( aElem.topsize.y - aElem.topsize.x ) / 2 ); aShape->SetStart( aElem.position + offset ); aShape->SetEnd( aElem.position - offset ); } else { VECTOR2I offset( ( aElem.topsize.x - aElem.topsize.y ) / 2, 0 ); aShape->SetStart( aElem.position + offset ); aShape->SetEnd( aElem.position - offset ); } if( aElem.direction != 0 ) aShape->Rotate( aElem.position, EDA_ANGLE( aElem.direction, DEGREES_T ) ); } break; case ALTIUM_PAD_SHAPE::OCTAGONAL: { // filled octagon aShape->SetShape( SHAPE_T::POLY ); aShape->SetFilled( true ); aShape->SetLayer( aLayer ); aShape->SetStroke( STROKE_PARAMS( 0 ) ); VECTOR2I p11 = aElem.position + VECTOR2I( aElem.topsize.x / 2, aElem.topsize.y / 2 ); VECTOR2I p12 = aElem.position + VECTOR2I( aElem.topsize.x / 2, -aElem.topsize.y / 2 ); VECTOR2I p22 = aElem.position + VECTOR2I( -aElem.topsize.x / 2, -aElem.topsize.y / 2 ); VECTOR2I p21 = aElem.position + VECTOR2I( -aElem.topsize.x / 2, aElem.topsize.y / 2 ); int chamfer = std::min( aElem.topsize.x, aElem.topsize.y ) / 4; VECTOR2I chamferX( chamfer, 0 ); VECTOR2I chamferY( 0, chamfer ); aShape->SetPolyPoints( { p11 - chamferX, p11 - chamferY, p12 + chamferY, p12 - chamferX, p22 + chamferX, p22 + chamferY, p21 - chamferY, p21 + chamferX } ); if( aElem.direction != 0. ) aShape->Rotate( aElem.position, EDA_ANGLE( aElem.direction, DEGREES_T ) ); } break; case ALTIUM_PAD_SHAPE::UNKNOWN: default: wxLogError( _( "Non-copper pad %s uses an unknown pad-shape." ), aElem.name ); break; } } void ALTIUM_PCB::ParseVias6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading vias..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); while( reader.GetRemainingBytes() >= 4 /* TODO: use Header section of file */ ) { checkpoint(); AVIA6 elem( reader ); std::unique_ptr via = std::make_unique( m_board ); via->SetPosition( elem.position ); via->SetWidth( elem.diameter ); via->SetDrill( elem.holesize ); via->SetNetCode( GetNetCode( elem.net ) ); via->SetLocked( elem.is_locked ); bool start_layer_outside = elem.layer_start == ALTIUM_LAYER::TOP_LAYER || elem.layer_start == ALTIUM_LAYER::BOTTOM_LAYER; bool end_layer_outside = elem.layer_end == ALTIUM_LAYER::TOP_LAYER || elem.layer_end == ALTIUM_LAYER::BOTTOM_LAYER; if( start_layer_outside && end_layer_outside ) { via->SetViaType( VIATYPE::THROUGH ); } else if( ( !start_layer_outside ) && ( !end_layer_outside ) ) { via->SetViaType( VIATYPE::BLIND_BURIED ); } else { via->SetViaType( VIATYPE::MICROVIA ); // TODO: always a microvia? } PCB_LAYER_ID start_klayer = GetKicadLayer( elem.layer_start ); PCB_LAYER_ID end_klayer = GetKicadLayer( elem.layer_end ); if( !IsCopperLayer( start_klayer ) || !IsCopperLayer( end_klayer ) ) { wxLogError( _( "Via from layer %d to %d uses a non-copper layer, which is not " "supported." ), elem.layer_start, elem.layer_end ); continue; // just assume through-hole instead. } // we need VIATYPE set! via->SetLayerPair( start_klayer, end_klayer ); m_board->Add( via.release(), ADD_MODE::APPEND ); } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( wxT( "Vias6 stream is not fully parsed" ) ); } void ALTIUM_PCB::ParseTracks6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading tracks..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); for( int primitiveIndex = 0; reader.GetRemainingBytes() >= 4; primitiveIndex++ ) { checkpoint(); ATRACK6 elem( reader ); if( elem.component == ALTIUM_COMPONENT_NONE ) { ConvertTracks6ToBoardItem( elem, primitiveIndex ); } else { FOOTPRINT* footprint = HelperGetFootprint( elem.component ); ConvertTracks6ToFootprintItem( footprint, elem, primitiveIndex, true ); } } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( "Tracks6 stream is not fully parsed" ); } void ALTIUM_PCB::ConvertTracks6ToBoardItem( const ATRACK6& aElem, const int aPrimitiveIndex ) { if( aElem.polygon != ALTIUM_POLYGON_NONE && aElem.polygon != ALTIUM_POLYGON_BOARD ) { if( m_polygons.size() <= aElem.polygon ) { // Can happen when reading old Altium files: just skip this item wxLogError( "ATRACK6 stream tries to access polygon id %u " "of %u existing polygons. Skip it", (unsigned) aElem.polygon, (unsigned)m_polygons.size() ); return; } ZONE* zone = m_polygons.at( aElem.polygon ); if( zone == nullptr ) { return; // we know the zone id, but because we do not know the layer we did not // add it! } PCB_LAYER_ID klayer = GetKicadLayer( aElem.layer ); if( klayer == UNDEFINED_LAYER ) return; // Just skip it for now. Users can fill it themselves. SHAPE_POLY_SET* fill = zone->GetFill( klayer ); PCB_SHAPE shape( nullptr, SHAPE_T::SEGMENT ); shape.SetStart( aElem.start ); shape.SetEnd( aElem.end ); shape.SetStroke( STROKE_PARAMS( aElem.width, LINE_STYLE::SOLID ) ); shape.EDA_SHAPE::TransformShapeToPolygon( *fill, 0, ARC_HIGH_DEF, ERROR_INSIDE ); // Will be simplified and fractured later zone->SetIsFilled( true ); zone->SetNeedRefill( false ); return; } if( aElem.is_keepout || aElem.layer == ALTIUM_LAYER::KEEP_OUT_LAYER || IsAltiumLayerAPlane( aElem.layer ) ) { // This is not the actual board item. We can use it to create the polygon for the region PCB_SHAPE shape( nullptr, SHAPE_T::SEGMENT ); shape.SetStart( aElem.start ); shape.SetEnd( aElem.end ); shape.SetStroke( STROKE_PARAMS( aElem.width, LINE_STYLE::SOLID ) ); HelperPcpShapeAsBoardKeepoutRegion( shape, aElem.layer, aElem.keepoutrestrictions ); } else { for( PCB_LAYER_ID klayer : GetKicadLayersToIterate( aElem.layer ) ) ConvertTracks6ToBoardItemOnLayer( aElem, klayer ); } for( const auto& layerExpansionMask : HelperGetSolderAndPasteMaskExpansions( ALTIUM_RECORD::TRACK, aPrimitiveIndex, aElem.layer ) ) { int width = aElem.width + ( layerExpansionMask.second * 2 ); if( width > 1 ) { std::unique_ptr seg = std::make_unique( m_board, SHAPE_T::SEGMENT ); seg->SetStart( aElem.start ); seg->SetEnd( aElem.end ); seg->SetStroke( STROKE_PARAMS( width, LINE_STYLE::SOLID ) ); seg->SetLayer( layerExpansionMask.first ); m_board->Add( seg.release(), ADD_MODE::APPEND ); } } } void ALTIUM_PCB::ConvertTracks6ToFootprintItem( FOOTPRINT* aFootprint, const ATRACK6& aElem, const int aPrimitiveIndex, const bool aIsBoardImport ) { if( aElem.polygon != ALTIUM_POLYGON_NONE ) { wxFAIL_MSG( wxString::Format( "Altium: Unexpected footprint Track with polygon id %u", (unsigned)aElem.polygon ) ); return; } if( aElem.is_keepout || aElem.layer == ALTIUM_LAYER::KEEP_OUT_LAYER || IsAltiumLayerAPlane( aElem.layer ) ) { // This is not the actual board item. We can use it to create the polygon for the region PCB_SHAPE shape( nullptr, SHAPE_T::SEGMENT ); shape.SetStart( aElem.start ); shape.SetEnd( aElem.end ); shape.SetStroke( STROKE_PARAMS( aElem.width, LINE_STYLE::SOLID ) ); HelperPcpShapeAsFootprintKeepoutRegion( aFootprint, shape, aElem.layer, aElem.keepoutrestrictions ); } else { for( PCB_LAYER_ID klayer : GetKicadLayersToIterate( aElem.layer ) ) { if( aIsBoardImport && IsCopperLayer( klayer ) && aElem.net != ALTIUM_NET_UNCONNECTED ) { // Special case: do to not lose net connections in footprints ConvertTracks6ToBoardItemOnLayer( aElem, klayer ); } else { ConvertTracks6ToFootprintItemOnLayer( aFootprint, aElem, klayer ); } } } for( const auto& layerExpansionMask : HelperGetSolderAndPasteMaskExpansions( ALTIUM_RECORD::TRACK, aPrimitiveIndex, aElem.layer ) ) { int width = aElem.width + ( layerExpansionMask.second * 2 ); if( width > 1 ) { std::unique_ptr seg = std::make_unique( aFootprint, SHAPE_T::SEGMENT ); seg->SetStart( aElem.start ); seg->SetEnd( aElem.end ); seg->SetStroke( STROKE_PARAMS( width, LINE_STYLE::SOLID ) ); seg->SetLayer( layerExpansionMask.first ); aFootprint->Add( seg.release(), ADD_MODE::APPEND ); } } } void ALTIUM_PCB::ConvertTracks6ToBoardItemOnLayer( const ATRACK6& aElem, PCB_LAYER_ID aLayer ) { if( IsCopperLayer( aLayer ) && aElem.net != ALTIUM_NET_UNCONNECTED ) { std::unique_ptr track = std::make_unique( m_board ); track->SetStart( aElem.start ); track->SetEnd( aElem.end ); track->SetWidth( aElem.width ); track->SetLayer( aLayer ); track->SetNetCode( GetNetCode( aElem.net ) ); m_board->Add( track.release(), ADD_MODE::APPEND ); } else { std::unique_ptr seg = std::make_unique( m_board, SHAPE_T::SEGMENT ); seg->SetStart( aElem.start ); seg->SetEnd( aElem.end ); seg->SetStroke( STROKE_PARAMS( aElem.width, LINE_STYLE::SOLID ) ); seg->SetLayer( aLayer ); m_board->Add( seg.release(), ADD_MODE::APPEND ); } } void ALTIUM_PCB::ConvertTracks6ToFootprintItemOnLayer( FOOTPRINT* aFootprint, const ATRACK6& aElem, PCB_LAYER_ID aLayer ) { std::unique_ptr seg = std::make_unique( aFootprint, SHAPE_T::SEGMENT ); seg->SetStart( aElem.start ); seg->SetEnd( aElem.end ); seg->SetStroke( STROKE_PARAMS( aElem.width, LINE_STYLE::SOLID ) ); seg->SetLayer( aLayer ); aFootprint->Add( seg.release(), ADD_MODE::APPEND ); } void ALTIUM_PCB::ParseWideStrings6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading unicode strings..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); m_unicodeStrings = reader.ReadWideStringTable(); if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( wxT( "WideStrings6 stream is not fully parsed" ) ); } void ALTIUM_PCB::ParseTexts6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading text..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); while( reader.GetRemainingBytes() >= 4 /* TODO: use Header section of file */ ) { checkpoint(); ATEXT6 elem( reader, m_unicodeStrings ); if( elem.component == ALTIUM_COMPONENT_NONE ) { ConvertTexts6ToBoardItem( elem ); } else { FOOTPRINT* footprint = HelperGetFootprint( elem.component ); ConvertTexts6ToFootprintItem( footprint, elem ); } } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( wxT( "Texts6 stream is not fully parsed" ) ); } void ALTIUM_PCB::ConvertTexts6ToBoardItem( const ATEXT6& aElem ) { if( aElem.fonttype == ALTIUM_TEXT_TYPE::BARCODE ) { wxLogError( _( "Ignored barcode on Altium layer %d (not yet supported)." ), aElem.layer ); return; } for( PCB_LAYER_ID klayer : GetKicadLayersToIterate( aElem.layer ) ) ConvertTexts6ToBoardItemOnLayer( aElem, klayer ); } void ALTIUM_PCB::ConvertTexts6ToFootprintItem( FOOTPRINT* aFootprint, const ATEXT6& aElem ) { if( aElem.fonttype == ALTIUM_TEXT_TYPE::BARCODE ) { if( !m_footprintName.IsEmpty() ) { wxLogError( _( "Error loading library '%s':\n" "Footprint %s contains barcode on Altium layer %d (not yet supported)." ), m_library, m_footprintName, aElem.layer ); } else { wxLogError( _( "Footprint %s contains barcode on Altium layer %d (not yet supported)." ), aFootprint->GetReference(), aElem.layer ); } return; } for( PCB_LAYER_ID klayer : GetKicadLayersToIterate( aElem.layer ) ) ConvertTexts6ToFootprintItemOnLayer( aFootprint, aElem, klayer ); } void ALTIUM_PCB::ConvertTexts6ToBoardItemOnLayer( const ATEXT6& aElem, PCB_LAYER_ID aLayer ) { std::unique_ptr pcbText = std::make_unique( m_board ); static const std::map variableMap = { { "LAYER_NAME", "LAYER" }, { "PRINT_DATE", "CURRENT_DATE"}, }; wxString kicadText = AltiumPcbSpecialStringsToKiCadStrings( aElem.text, variableMap ); pcbText->SetText(kicadText); pcbText->SetLayer( aLayer ); pcbText->SetPosition( aElem.position ); pcbText->SetTextAngle( EDA_ANGLE( aElem.rotation, DEGREES_T ) ); ConvertTexts6ToEdaTextSettings( aElem, *pcbText ); m_board->Add( pcbText.release(), ADD_MODE::APPEND ); } void ALTIUM_PCB::ConvertTexts6ToFootprintItemOnLayer( FOOTPRINT* aFootprint, const ATEXT6& aElem, PCB_LAYER_ID aLayer ) { PCB_TEXT* fpText; if( aElem.isDesignator ) { fpText = &aFootprint->Reference(); // TODO: handle multiple layers } else if( aElem.isComment ) { fpText = &aFootprint->Value(); // TODO: handle multiple layers } else { fpText = new PCB_TEXT( aFootprint ); aFootprint->Add( fpText, ADD_MODE::APPEND ); } static const std::map variableMap = { { "DESIGNATOR", "REFERENCE" }, { "COMMENT", "VALUE" }, { "VALUE", "ALTIUM_VALUE" }, { "LAYER_NAME", "LAYER" }, { "PRINT_DATE", "CURRENT_DATE"}, }; wxString kicadText = AltiumPcbSpecialStringsToKiCadStrings( aElem.text, variableMap ); fpText->SetText(kicadText); fpText->SetKeepUpright( false ); fpText->SetLayer( aLayer ); fpText->SetPosition( aElem.position ); fpText->SetTextAngle( EDA_ANGLE( aElem.rotation, DEGREES_T ) ); ConvertTexts6ToEdaTextSettings( aElem, *fpText ); } void ALTIUM_PCB::ConvertTexts6ToEdaTextSettings( const ATEXT6& aElem, EDA_TEXT& aEdaText ) { aEdaText.SetTextSize( VECTOR2I( aElem.height, aElem.height ) ); // TODO: parse text width if( aElem.fonttype == ALTIUM_TEXT_TYPE::TRUETYPE ) { KIFONT::FONT* font = KIFONT::FONT::GetFont( aElem.fontname, aElem.isBold, aElem.isItalic ); aEdaText.SetFont( font ); if( font->IsOutline() ) { // TODO: why is this required? Somehow, truetype size is calculated differently if( font->GetName().Contains( wxS( "Arial" ) ) ) aEdaText.SetTextSize( VECTOR2I( aElem.height * 0.63, aElem.height * 0.63 ) ); else aEdaText.SetTextSize( VECTOR2I( aElem.height * 0.5, aElem.height * 0.5 ) ); } } aEdaText.SetTextThickness( aElem.strokewidth ); aEdaText.SetBoldFlag( aElem.isBold ); aEdaText.SetItalic( aElem.isItalic ); aEdaText.SetMirrored( aElem.isMirrored ); // Altium position always specifies the bottom left corner aEdaText.SetHorizJustify( GR_TEXT_H_ALIGN_LEFT ); aEdaText.SetVertJustify( GR_TEXT_V_ALIGN_BOTTOM ); // TODO: correct the position and set proper justification } void ALTIUM_PCB::ParseFills6Data( const ALTIUM_COMPOUND_FILE& aAltiumPcbFile, const CFB::COMPOUND_FILE_ENTRY* aEntry ) { if( m_progressReporter ) m_progressReporter->Report( _( "Loading rectangles..." ) ); ALTIUM_BINARY_PARSER reader( aAltiumPcbFile, aEntry ); while( reader.GetRemainingBytes() >= 4 /* TODO: use Header section of file */ ) { checkpoint(); AFILL6 elem( reader ); if( elem.component == ALTIUM_COMPONENT_NONE ) { ConvertFills6ToBoardItem( elem ); } else { FOOTPRINT* footprint = HelperGetFootprint( elem.component ); ConvertFills6ToFootprintItem( footprint, elem, true ); } } if( reader.GetRemainingBytes() != 0 ) THROW_IO_ERROR( "Fills6 stream is not fully parsed" ); } void ALTIUM_PCB::ConvertFills6ToBoardItem( const AFILL6& aElem ) { if( aElem.is_keepout || aElem.layer == ALTIUM_LAYER::KEEP_OUT_LAYER ) { // This is not the actual board item. We can use it to create the polygon for the region PCB_SHAPE shape( nullptr, SHAPE_T::RECTANGLE ); shape.SetStart( aElem.pos1 ); shape.SetEnd( aElem.pos2 ); shape.SetFilled( true ); shape.SetStroke( STROKE_PARAMS( 0, LINE_STYLE::SOLID ) ); if( aElem.rotation != 0. ) { VECTOR2I center( ( aElem.pos1.x + aElem.pos2.x ) / 2, ( aElem.pos1.y + aElem.pos2.y ) / 2 ); shape.Rotate( center, EDA_ANGLE( aElem.rotation, DEGREES_T ) ); } HelperPcpShapeAsBoardKeepoutRegion( shape, aElem.layer, aElem.keepoutrestrictions ); } else { for( PCB_LAYER_ID klayer : GetKicadLayersToIterate( aElem.layer ) ) ConvertFills6ToBoardItemOnLayer( aElem, klayer ); } } void ALTIUM_PCB::ConvertFills6ToFootprintItem( FOOTPRINT* aFootprint, const AFILL6& aElem, const bool aIsBoardImport ) { if( aElem.is_keepout || aElem.layer == ALTIUM_LAYER::KEEP_OUT_LAYER ) // TODO: what about plane layers? { // This is not the actual board item. We can use it to create the polygon for the region PCB_SHAPE shape( nullptr, SHAPE_T::RECTANGLE ); shape.SetStart( aElem.pos1 ); shape.SetEnd( aElem.pos2 ); shape.SetFilled( true ); shape.SetStroke( STROKE_PARAMS( 0, LINE_STYLE::SOLID ) ); if( aElem.rotation != 0. ) { VECTOR2I center( ( aElem.pos1.x + aElem.pos2.x ) / 2, ( aElem.pos1.y + aElem.pos2.y ) / 2 ); shape.Rotate( center, EDA_ANGLE( aElem.rotation, DEGREES_T ) ); } HelperPcpShapeAsFootprintKeepoutRegion( aFootprint, shape, aElem.layer, aElem.keepoutrestrictions ); } else if( aIsBoardImport && IsAltiumLayerCopper( aElem.layer ) && aElem.net != ALTIUM_NET_UNCONNECTED ) { // Special case: do to not lose net connections in footprints for( PCB_LAYER_ID klayer : GetKicadLayersToIterate( aElem.layer ) ) ConvertFills6ToBoardItemOnLayer( aElem, klayer ); } else { for( PCB_LAYER_ID klayer : GetKicadLayersToIterate( aElem.layer ) ) ConvertFills6ToFootprintItemOnLayer( aFootprint, aElem, klayer ); } } void ALTIUM_PCB::ConvertFills6ToBoardItemOnLayer( const AFILL6& aElem, PCB_LAYER_ID aLayer ) { std::unique_ptr fill = std::make_unique( m_board, SHAPE_T::RECTANGLE ); fill->SetFilled( true ); fill->SetLayer( aLayer ); fill->SetStroke( STROKE_PARAMS( 0 ) ); fill->SetStart( aElem.pos1 ); fill->SetEnd( aElem.pos2 ); if( IsCopperLayer( aLayer ) && aElem.net != ALTIUM_NET_UNCONNECTED ) { fill->SetNetCode( GetNetCode( aElem.net ) ); } if( aElem.rotation != 0. ) { // TODO: Do we need SHAPE_T::POLY for non 90° rotations? VECTOR2I center( ( aElem.pos1.x + aElem.pos2.x ) / 2, ( aElem.pos1.y + aElem.pos2.y ) / 2 ); fill->Rotate( center, EDA_ANGLE( aElem.rotation, DEGREES_T ) ); } m_board->Add( fill.release(), ADD_MODE::APPEND ); } void ALTIUM_PCB::ConvertFills6ToFootprintItemOnLayer( FOOTPRINT* aFootprint, const AFILL6& aElem, PCB_LAYER_ID aLayer ) { std::unique_ptr fill = std::make_unique( aFootprint, SHAPE_T::RECTANGLE ); fill->SetFilled( true ); fill->SetLayer( aLayer ); fill->SetStroke( STROKE_PARAMS( 0 ) ); fill->SetStart( aElem.pos1 ); fill->SetEnd( aElem.pos2 ); if( aElem.rotation != 0. ) { // TODO: Do we need SHAPE_T::POLY for non 90° rotations? VECTOR2I center( ( aElem.pos1.x + aElem.pos2.x ) / 2, ( aElem.pos1.y + aElem.pos2.y ) / 2 ); fill->Rotate( center, EDA_ANGLE( aElem.rotation, DEGREES_T ) ); } aFootprint->Add( fill.release(), ADD_MODE::APPEND ); } void ALTIUM_PCB::HelperSetZoneLayers( ZONE& aZone, const ALTIUM_LAYER aAltiumLayer ) { LSET layerSet; for( PCB_LAYER_ID klayer : GetKicadLayersToIterate( aAltiumLayer ) ) layerSet.set( klayer ); aZone.SetLayerSet( layerSet ); } void ALTIUM_PCB::HelperSetZoneKeepoutRestrictions( ZONE& aZone, const uint8_t aKeepoutRestrictions ) { bool keepoutRestrictionVia = ( aKeepoutRestrictions & 0x01 ) != 0; bool keepoutRestrictionTrack = ( aKeepoutRestrictions & 0x02 ) != 0; bool keepoutRestrictionCopper = ( aKeepoutRestrictions & 0x04 ) != 0; bool keepoutRestrictionSMDPad = ( aKeepoutRestrictions & 0x08 ) != 0; bool keepoutRestrictionTHPad = ( aKeepoutRestrictions & 0x10 ) != 0; aZone.SetDoNotAllowVias( keepoutRestrictionVia ); aZone.SetDoNotAllowTracks( keepoutRestrictionTrack ); aZone.SetDoNotAllowCopperPour( keepoutRestrictionCopper ); aZone.SetDoNotAllowPads( keepoutRestrictionSMDPad && keepoutRestrictionTHPad ); aZone.SetDoNotAllowFootprints( false ); } void ALTIUM_PCB::HelperPcpShapeAsBoardKeepoutRegion( const PCB_SHAPE& aShape, const ALTIUM_LAYER aAltiumLayer, const uint8_t aKeepoutRestrictions ) { std::unique_ptr zone = std::make_unique( m_board ); zone->SetIsRuleArea( true ); HelperSetZoneLayers( *zone, aAltiumLayer ); HelperSetZoneKeepoutRestrictions( *zone, aKeepoutRestrictions ); aShape.EDA_SHAPE::TransformShapeToPolygon( *zone->Outline(), 0, ARC_HIGH_DEF, ERROR_INSIDE ); zone->SetBorderDisplayStyle( ZONE_BORDER_DISPLAY_STYLE::DIAGONAL_EDGE, ZONE::GetDefaultHatchPitch(), true ); m_board->Add( zone.release(), ADD_MODE::APPEND ); } void ALTIUM_PCB::HelperPcpShapeAsFootprintKeepoutRegion( FOOTPRINT* aFootprint, const PCB_SHAPE& aShape, const ALTIUM_LAYER aAltiumLayer, const uint8_t aKeepoutRestrictions ) { std::unique_ptr zone = std::make_unique( aFootprint ); zone->SetIsRuleArea( true ); HelperSetZoneLayers( *zone, aAltiumLayer ); HelperSetZoneKeepoutRestrictions( *zone, aKeepoutRestrictions ); aShape.EDA_SHAPE::TransformShapeToPolygon( *zone->Outline(), 0, ARC_HIGH_DEF, ERROR_INSIDE ); zone->SetBorderDisplayStyle( ZONE_BORDER_DISPLAY_STYLE::DIAGONAL_EDGE, ZONE::GetDefaultHatchPitch(), true ); // TODO: zone->SetLocalCoord(); missing? aFootprint->Add( zone.release(), ADD_MODE::APPEND ); } std::vector> ALTIUM_PCB::HelperGetSolderAndPasteMaskExpansions( const ALTIUM_RECORD aType, const int aPrimitiveIndex, const ALTIUM_LAYER aAltiumLayer ) { if( m_extendedPrimitiveInformationMaps.count( aType ) == 0 ) return {}; // there is nothing to parse auto elems = m_extendedPrimitiveInformationMaps[ALTIUM_RECORD::TRACK].equal_range( aPrimitiveIndex ); if( elems.first == elems.second ) return {}; // there is nothing to parse std::vector> layerExpansionPairs; for( auto it = elems.first; it != elems.second; ++it ) { const AEXTENDED_PRIMITIVE_INFORMATION& pInf = it->second; if( pInf.type == AEXTENDED_PRIMITIVE_INFORMATION_TYPE::MASK ) { if( pInf.soldermaskexpansionmode == ALTIUM_MODE::MANUAL || pInf.soldermaskexpansionmode == ALTIUM_MODE::RULE ) { // TODO: what layers can lead to solder or paste mask usage? E.g. KEEP_OUT_LAYER and other top/bottom layers if( aAltiumLayer == ALTIUM_LAYER::TOP_LAYER || aAltiumLayer == ALTIUM_LAYER::MULTI_LAYER ) { layerExpansionPairs.emplace_back( F_Mask, pInf.soldermaskexpansionmanual ); } if( aAltiumLayer == ALTIUM_LAYER::BOTTOM_LAYER || aAltiumLayer == ALTIUM_LAYER::MULTI_LAYER ) { layerExpansionPairs.emplace_back( B_Mask, pInf.soldermaskexpansionmanual ); } } if( pInf.pastemaskexpansionmode == ALTIUM_MODE::MANUAL || pInf.pastemaskexpansionmode == ALTIUM_MODE::RULE ) { if( aAltiumLayer == ALTIUM_LAYER::TOP_LAYER || aAltiumLayer == ALTIUM_LAYER::MULTI_LAYER ) { layerExpansionPairs.emplace_back( F_Paste, pInf.pastemaskexpansionmanual ); } if( aAltiumLayer == ALTIUM_LAYER::BOTTOM_LAYER || aAltiumLayer == ALTIUM_LAYER::MULTI_LAYER ) { layerExpansionPairs.emplace_back( B_Paste, pInf.pastemaskexpansionmanual ); } } } } return layerExpansionPairs; }