/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2023 Alex Shvartzkop * Copyright (C) 2023 KiCad Developers, see AUTHORS.txt for contributors. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, you may find one here: * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * or you may search the http://www.gnu.org website for the version 2 license, * or you may write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include PCB_IO_EASYEDA::PCB_IO_EASYEDA() : PCB_IO( wxS( "EasyEDA (JLCEDA) Standard" ) ) { } PCB_IO_EASYEDA::~PCB_IO_EASYEDA() { } static bool FindBoardInStream( const wxString& aName, wxInputStream& aStream, nlohmann::json& aOut, EASYEDA::DOCUMENT& aDoc ) { if( aName.Lower().EndsWith( wxS( ".json" ) ) ) { wxStdInputStream sin( aStream ); nlohmann::json js = nlohmann::json::parse( sin, nullptr, false ); if( js.is_discarded() ) return false; EASYEDA::DOCUMENT doc = js.get(); if( doc.head.docType == EASYEDA::DOC_TYPE::PCB || doc.head.docType == EASYEDA::DOC_TYPE::PCB_MODULE || doc.head.docType == EASYEDA::DOC_TYPE::PCB_COMPONENT ) { aOut = js; aDoc = doc; return true; } } else if( aName.Lower().EndsWith( wxS( ".zip" ) ) ) { std::shared_ptr entry; wxZipInputStream zip( aStream ); if( !zip.IsOk() ) return false; while( entry.reset( zip.GetNextEntry() ), entry.get() != NULL ) { wxString name = entry->GetName(); if( FindBoardInStream( name, zip, aOut, aDoc ) ) return true; } } return false; } bool PCB_IO_EASYEDA::CanReadBoard( const wxString& aFileName ) const { if( !PCB_IO::CanReadBoard( aFileName ) ) return false; try { wxFFileInputStream in( aFileName ); nlohmann::json js; EASYEDA::DOCUMENT doc; return FindBoardInStream( aFileName, in, js, doc ); } catch( nlohmann::json::exception& ) { } catch( std::exception& ) { } return false; } bool PCB_IO_EASYEDA::CanReadFootprint( const wxString& aFileName ) const { return CanReadBoard( aFileName ); } bool PCB_IO_EASYEDA::CanReadLibrary( const wxString& aFileName ) const { return CanReadBoard( aFileName ); } BOARD* PCB_IO_EASYEDA::LoadBoard( const wxString& aFileName, BOARD* aAppendToMe, const STRING_UTF8_MAP* aProperties, PROJECT* aProject ) { m_loadedFootprints.clear(); m_props = aProperties; m_board = aAppendToMe ? aAppendToMe : new BOARD(); // Give the filename to the board if it's new if( !aAppendToMe ) m_board->SetFileName( aFileName ); if( m_progressReporter ) { m_progressReporter->Report( wxString::Format( _( "Loading %s..." ), aFileName ) ); if( !m_progressReporter->KeepRefreshing() ) THROW_IO_ERROR( _( "Open cancelled by user." ) ); } PCB_IO_EASYEDA_PARSER parser( nullptr ); try { wxFFileInputStream in( aFileName ); nlohmann::json js; EASYEDA::DOCUMENT doc; if( !FindBoardInStream( aFileName, in, js, doc ) ) { THROW_IO_ERROR( wxString::Format( _( "Unable to find a valid board in '%s'" ), aFileName ) ); } EASYEDA::DOCUMENT_PCB pcbDoc = js.get(); const int innerStart = 21; const int innerEnd = 52; int maxLayer = innerStart; std::map layerNames; for( const wxString& layerLine : pcbDoc.layers ) { wxArrayString parts = wxSplit( layerLine, '~', '\0' ); int layerId = wxAtoi( parts[0] ); wxString layerName = parts[1]; wxString layerColor = parts[2]; wxString visible = parts[3]; wxString active = parts[4]; bool enabled = parts[5] != wxS( "false" ); if( layerId >= innerStart && layerId <= innerEnd && enabled ) maxLayer = layerId + 1; layerNames[parser.LayerToKi( parts[0] )] = layerName; } m_board->SetCopperLayerCount( 2 + maxLayer - innerStart ); for( auto& [klayer, name] : layerNames ) m_board->SetLayerName( klayer, name ); BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings(); std::shared_ptr defNetclass = bds.m_NetSettings->m_DefaultNetClass; if( pcbDoc.DRCRULE ) { std::map& rules = *pcbDoc.DRCRULE; if( auto defRules = get_opt( rules, "Default" ) ) { wxString key; key = wxS( "trackWidth" ); if( defRules->find( key ) != defRules->end() && defRules->at( key ).is_number() ) { double val = parser.ScaleSize( defRules->at( key ) ); defNetclass->SetTrackWidth( val ); } key = wxS( "clearance" ); if( defRules->find( key ) != defRules->end() && defRules->at( key ).is_number() ) { double val = parser.ScaleSize( defRules->at( key ) ); defNetclass->SetClearance( val ); } key = wxS( "viaHoleD" ); if( defRules->find( key ) != defRules->end() && defRules->at( key ).is_number() ) { double val = parser.ScaleSize( defRules->at( key ) ); defNetclass->SetViaDrill( val ); } key = wxS( "viaHoleDiameter" ); // Yes, this is via diameter, not drill diameter if( defRules->find( key ) != defRules->end() && defRules->at( key ).is_number() ) { double val = parser.ScaleSize( defRules->at( key ) ); defNetclass->SetViaDiameter( val ); } } } VECTOR2D origin( doc.head.x, doc.head.y ); parser.ParseBoard( m_board, origin, m_loadedFootprints, doc.shape ); // Center the board BOX2I outlineBbox = m_board->ComputeBoundingBox( true ); PAGE_INFO pageInfo = m_board->GetPageSettings(); VECTOR2D pageCenter( pcbIUScale.MilsToIU( pageInfo.GetWidthMils() / 2 ), pcbIUScale.MilsToIU( pageInfo.GetHeightMils() / 2 ) ); VECTOR2D offset = pageCenter - outlineBbox.GetCenter(); int alignGrid = pcbIUScale.mmToIU( 10 ); offset.x = KiROUND( offset.x / alignGrid ) * alignGrid; offset.y = KiROUND( offset.y / alignGrid ) * alignGrid; m_board->Move( offset ); bds.SetAuxOrigin( offset ); return m_board; } catch( nlohmann::json::exception& e ) { THROW_IO_ERROR( wxString::Format( _( "Error loading board '%s': %s" ), aFileName, e.what() ) ); } catch( std::exception& e ) { THROW_IO_ERROR( wxString::Format( _( "Error loading board '%s': %s" ), aFileName, e.what() ) ); } return m_board; } long long PCB_IO_EASYEDA::GetLibraryTimestamp( const wxString& aLibraryPath ) const { return 0; } void PCB_IO_EASYEDA::FootprintEnumerate( wxArrayString& aFootprintNames, const wxString& aLibraryPath, bool aBestEfforts, const STRING_UTF8_MAP* aProperties ) { try { wxFFileInputStream in( aLibraryPath ); nlohmann::json js; EASYEDA::DOCUMENT doc; if( !FindBoardInStream( aLibraryPath, in, js, doc ) ) { THROW_IO_ERROR( wxString::Format( _( "Unable to find valid footprints in '%s'" ), aLibraryPath ) ); } if( doc.head.docType == EASYEDA::DOC_TYPE::PCB || doc.head.docType == EASYEDA::DOC_TYPE::PCB_MODULE ) { for( wxString shap : doc.shape ) { shap.Replace( wxS( "#@$" ), "\n" ); wxArrayString parts = wxSplit( shap, '\n', '\0' ); if( parts.size() < 1 ) continue; wxArrayString paramsRoot = wxSplit( parts[0], '~', '\0' ); if( paramsRoot.size() < 1 ) continue; wxString rootType = paramsRoot[0]; if( rootType == wxS( "LIB" ) ) { if( paramsRoot.size() < 4 ) continue; wxString packageName = wxString::Format( wxS( "Unknown_%s_%s" ), paramsRoot[1], paramsRoot[2] ); wxArrayString paramParts = wxSplit( paramsRoot[3], '`', '\0' ); std::map paramMap; for( int i = 1; i < paramParts.size(); i += 2 ) { wxString key = paramParts[i - 1]; wxString value = paramParts[i]; if( key == wxS( "package" ) ) packageName = value; paramMap[key] = value; } aFootprintNames.Add( packageName ); } } } else if( doc.head.docType == EASYEDA::DOC_TYPE::PCB_COMPONENT ) { EASYEDA::DOCUMENT_PCB pcbDoc = js.get(); wxString packageName = wxString::Format( wxS( "Unknown_%s" ), pcbDoc.uuid.value_or( wxS( "Unknown" ) ) ); if( pcbDoc.c_para ) { packageName = get_def( *pcbDoc.c_para, wxS( "package" ), packageName ); } aFootprintNames.Add( packageName ); } } catch( nlohmann::json::exception& e ) { THROW_IO_ERROR( wxString::Format( _( "Error enumerating footprints in library '%s': %s" ), aLibraryPath, e.what() ) ); } catch( std::exception& e ) { THROW_IO_ERROR( wxString::Format( _( "Error enumerating footprints in library '%s': %s" ), aLibraryPath, e.what() ) ); } } FOOTPRINT* PCB_IO_EASYEDA::FootprintLoad( const wxString& aLibraryPath, const wxString& aFootprintName, bool aKeepUUID, const STRING_UTF8_MAP* aProperties ) { PCB_IO_EASYEDA_PARSER parser( nullptr ); m_loadedFootprints.clear(); try { wxFFileInputStream in( aLibraryPath ); nlohmann::json js; EASYEDA::DOCUMENT doc; if( !FindBoardInStream( aLibraryPath, in, js, doc ) ) { THROW_IO_ERROR( wxString::Format( _( "Unable to find valid footprints in '%s'" ), aLibraryPath ) ); } if( doc.head.docType == EASYEDA::DOC_TYPE::PCB || doc.head.docType == EASYEDA::DOC_TYPE::PCB_MODULE ) { for( wxString shap : doc.shape ) { if( !shap.Contains( wxS( "LIB" ) ) ) continue; shap.Replace( wxS( "#@$" ), "\n" ); wxArrayString parts = wxSplit( shap, '\n', '\0' ); if( parts.size() < 1 ) continue; wxArrayString paramsRoot = wxSplit( parts[0], '~', '\0' ); if( paramsRoot.size() < 1 ) continue; wxString rootType = paramsRoot[0]; if( rootType == wxS( "LIB" ) ) { if( paramsRoot.size() < 4 ) continue; VECTOR2D origin( parser.Convert( paramsRoot[1] ), parser.Convert( paramsRoot[2] ) ); wxString packageName = wxString::Format( wxS( "Unknown_%s_%s" ), paramsRoot[1], paramsRoot[2] ); wxArrayString paramParts = wxSplit( paramsRoot[3], '`', '\0' ); std::map paramMap; for( int i = 1; i < paramParts.size(); i += 2 ) { wxString key = paramParts[i - 1]; wxString value = paramParts[i]; if( key == wxS( "package" ) ) packageName = value; paramMap[key] = value; } EDA_ANGLE orientation; if( !paramsRoot[4].IsEmpty() ) orientation = EDA_ANGLE( parser.Convert( paramsRoot[4] ), DEGREES_T ); int layer = 1; if( !paramsRoot[7].IsEmpty() ) layer = parser.Convert( paramsRoot[7] ); if( packageName == aFootprintName ) { parts.RemoveAt( 0 ); FOOTPRINT* footprint = parser.ParseFootprint( origin, orientation, layer, nullptr, paramMap, m_loadedFootprints, parts ); if( !footprint ) return nullptr; footprint->Reference().SetPosition( VECTOR2I() ); footprint->Reference().SetTextAngle( ANGLE_0 ); footprint->Reference().SetVisible( true ); footprint->Value().SetPosition( VECTOR2I() ); footprint->Value().SetTextAngle( ANGLE_0 ); footprint->Value().SetVisible( true ); footprint->AutoPositionFields(); return footprint; } } } } else if( doc.head.docType == EASYEDA::DOC_TYPE::PCB_COMPONENT ) { EASYEDA::DOCUMENT_PCB pcbDoc = js.get(); wxString packageName = wxString::Format( wxS( "Unknown_%s" ), pcbDoc.uuid.value_or( wxS( "Unknown" ) ) ); if( pcbDoc.c_para ) { packageName = get_def( *pcbDoc.c_para, wxS( "package" ), packageName ); if( packageName != aFootprintName ) return nullptr; VECTOR2D origin( doc.head.x, doc.head.y ); FOOTPRINT* footprint = parser.ParseFootprint( origin, ANGLE_0, F_Cu, nullptr, *pcbDoc.c_para, m_loadedFootprints, doc.shape ); if( !footprint ) return nullptr; footprint->Reference().SetPosition( VECTOR2I() ); footprint->Reference().SetTextAngle( ANGLE_0 ); footprint->Reference().SetVisible( true ); footprint->Value().SetPosition( VECTOR2I() ); footprint->Value().SetTextAngle( ANGLE_0 ); footprint->Value().SetVisible( true ); footprint->AutoPositionFields(); return footprint; } } } catch( nlohmann::json::exception& e ) { THROW_IO_ERROR( wxString::Format( _( "Error reading footprint '%s' from library '%s': %s" ), aFootprintName, aLibraryPath, e.what() ) ); } catch( std::exception& e ) { THROW_IO_ERROR( wxString::Format( _( "Error reading footprint '%s' from library '%s': %s" ), aFootprintName, aLibraryPath, e.what() ) ); } return nullptr; } std::vector PCB_IO_EASYEDA::GetImportedCachedLibraryFootprints() { std::vector result; for( auto& [fpUuid, footprint] : m_loadedFootprints ) { result.push_back( static_cast( footprint->Clone() ) ); } return result; }