/* * 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 "pcb_easyedapro_parser.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 #include #include #include #include static const wxString QUERY_MODEL_UUID_KEY = wxS( "JLC_3DModel_Q" ); static const wxString MODEL_SIZE_KEY = wxS( "JLC_3D_Size" ); static const int SHAPE_JOIN_DISTANCE = pcbIUScale.mmToIU( 1.5 ); double PCB_EASYEDAPRO_PARSER::Convert( wxString aValue ) { double value = 0; if( !aValue.ToCDouble( &value ) ) THROW_IO_ERROR( wxString::Format( _( "Failed to parse value: '%s'" ), aValue ) ); return value; } PCB_EASYEDAPRO_PARSER::PCB_EASYEDAPRO_PARSER( BOARD* aBoard, PROGRESS_REPORTER* aProgressReporter ) { m_board = aBoard; } PCB_EASYEDAPRO_PARSER::~PCB_EASYEDAPRO_PARSER() { } PCB_LAYER_ID PCB_EASYEDAPRO_PARSER::LayerToKi( int aLayer ) { switch( aLayer ) { case 1: return F_Cu; case 2: return B_Cu; case 3: return F_SilkS; case 4: return B_SilkS; case 5: return F_Mask; case 6: return B_Mask; case 7: return F_Paste; case 8: return B_Paste; case 9: return F_Fab; case 10: return B_Fab; case 11: return Edge_Cuts; case 12: return Edge_Cuts; // Multi case 13: return Dwgs_User; case 14: return Eco2_User; case 15: return In1_Cu; case 16: return In2_Cu; case 17: return In3_Cu; case 18: return In4_Cu; case 19: return In5_Cu; case 20: return In6_Cu; case 21: return In7_Cu; case 22: return In8_Cu; case 23: return In9_Cu; case 24: return In10_Cu; case 25: return In11_Cu; case 26: return In12_Cu; case 27: return In13_Cu; case 28: return In14_Cu; case 29: return In15_Cu; case 30: return In16_Cu; case 31: return In17_Cu; case 32: return In18_Cu; case 33: return In19_Cu; case 34: return In20_Cu; case 35: return In21_Cu; case 36: return In22_Cu; case 37: return In23_Cu; case 38: return In24_Cu; case 39: return In25_Cu; case 40: return In26_Cu; case 41: return In27_Cu; case 42: return In28_Cu; case 43: return In29_Cu; case 44: return In30_Cu; case 48: return User_2; // Component shape layer case 49: return User_3; // Component marking case 53: return User_4; // 3D shell outline case 54: return User_5; // 3D shell top case 55: return User_6; // 3D shell bot case 56: return User_7; // Drill drawing default: break; } return User_1; } static void AlignText( EDA_TEXT* text, int align ) { switch( align ) { case 1: text->SetVertJustify( GR_TEXT_V_ALIGN_TOP ); text->SetHorizJustify( GR_TEXT_H_ALIGN_LEFT ); break; case 2: text->SetVertJustify( GR_TEXT_V_ALIGN_CENTER ); text->SetHorizJustify( GR_TEXT_H_ALIGN_LEFT ); break; case 3: text->SetVertJustify( GR_TEXT_V_ALIGN_BOTTOM ); text->SetHorizJustify( GR_TEXT_H_ALIGN_LEFT ); break; case 4: text->SetVertJustify( GR_TEXT_V_ALIGN_TOP ); text->SetHorizJustify( GR_TEXT_H_ALIGN_CENTER ); break; case 5: text->SetVertJustify( GR_TEXT_V_ALIGN_CENTER ); text->SetHorizJustify( GR_TEXT_H_ALIGN_CENTER ); break; case 6: text->SetVertJustify( GR_TEXT_V_ALIGN_BOTTOM ); text->SetHorizJustify( GR_TEXT_H_ALIGN_CENTER ); break; case 7: text->SetVertJustify( GR_TEXT_V_ALIGN_TOP ); text->SetHorizJustify( GR_TEXT_H_ALIGN_RIGHT ); break; case 8: text->SetVertJustify( GR_TEXT_V_ALIGN_CENTER ); text->SetHorizJustify( GR_TEXT_H_ALIGN_RIGHT ); break; case 9: text->SetVertJustify( GR_TEXT_V_ALIGN_BOTTOM ); text->SetHorizJustify( GR_TEXT_H_ALIGN_RIGHT ); break; } } void PCB_EASYEDAPRO_PARSER::fillFootprintModelInfo( FOOTPRINT* footprint, const wxString& modelUuid, const wxString& modelTitle, const wxString& modelTransform ) const { // TODO: make this path configurable? const wxString easyedaModelDir = wxS( "EASYEDA_MODELS" ); const wxString kicadModelPrefix = wxS( "${KIPRJMOD}/" ) + easyedaModelDir + wxS( "/" ); VECTOR3D kmodelOffset; VECTOR3D kmodelRotation; if( !modelUuid.IsEmpty() && !footprint->GetFieldByName( QUERY_MODEL_UUID_KEY ) ) { PCB_FIELD field( footprint, footprint->GetFieldCount(), QUERY_MODEL_UUID_KEY ); field.SetLayer( Cmts_User ); field.SetVisible( false ); field.SetText( modelUuid ); footprint->AddField( field ); } if( !modelTransform.IsEmpty() && !footprint->GetFieldByName( MODEL_SIZE_KEY ) ) { wxArrayString arr = wxSplit( modelTransform, ',', '\0' ); double fitXmm = pcbIUScale.IUTomm( ScaleSize( Convert( arr[0] ) ) ); double fitYmm = pcbIUScale.IUTomm( ScaleSize( Convert( arr[1] ) ) ); if( fitXmm > 0.0 && fitYmm > 0.0 ) { PCB_FIELD field( footprint, footprint->GetFieldCount(), MODEL_SIZE_KEY ); field.SetLayer( Cmts_User ); field.SetVisible( false ); field.SetText( wxString::FromCDouble( fitXmm ) + wxS( " " ) + wxString::FromCDouble( fitYmm ) ); footprint->AddField( field ); } // TODO: other axes kmodelRotation.z = -Convert( arr[3] ); kmodelOffset.x = pcbIUScale.IUTomm( ScaleSize( Convert( arr[6] ) ) ); kmodelOffset.y = pcbIUScale.IUTomm( ScaleSize( Convert( arr[7] ) ) ); kmodelOffset.z = pcbIUScale.IUTomm( ScaleSize( Convert( arr[8] ) ) ); } if( !modelTitle.IsEmpty() && footprint->Models().empty() ) { FP_3DMODEL model; model.m_Filename = kicadModelPrefix + EscapeString( modelTitle, ESCAPE_CONTEXT::CTX_FILENAME ) + wxS( ".step" ); model.m_Offset = kmodelOffset; model.m_Rotation = kmodelRotation; footprint->Models().push_back( model ); } } std::vector> PCB_EASYEDAPRO_PARSER::ParsePoly( BOARD_ITEM_CONTAINER* aContainer, nlohmann::json polyData, bool aClosed, bool aInFill ) const { std::vector> results; VECTOR2D prevPt; for( int i = 0; i < polyData.size(); i++ ) { nlohmann::json val = polyData.at( i ); if( val.is_string() ) { wxString str = val; if( str == wxS( "CIRCLE" ) ) { VECTOR2D center; center.x = ( polyData.at( ++i ) ); center.y = ( polyData.at( ++i ) ); double r = ( polyData.at( ++i ) ); std::unique_ptr shape = std::make_unique( aContainer, SHAPE_T::CIRCLE ); shape->SetCenter( ScalePos( center ) ); shape->SetEnd( ScalePos( center + VECTOR2D( r, 0 ) ) ); shape->SetFilled( aClosed ); results.emplace_back( std::move( shape ) ); } else if( str == wxS( "R" ) ) { VECTOR2D start, size; start.x = ( polyData.at( ++i ) ); start.y = ( polyData.at( ++i ) ); size.x = ( polyData.at( ++i ) ); size.y = -( polyData.at( ++i ).get() ); double angle = polyData.at( ++i ); double cr = ( i + 1 ) < polyData.size() ? polyData.at( ++i ).get() : 0; if( cr == 0 ) { std::unique_ptr shape = std::make_unique( aContainer, SHAPE_T::RECTANGLE ); shape->SetStart( ScalePos( start ) ); shape->SetEnd( ScalePos( start + size ) ); shape->SetFilled( aClosed ); shape->Rotate( ScalePos( start ), EDA_ANGLE( angle, DEGREES_T ) ); results.emplace_back( std::move( shape ) ); } else { VECTOR2D end = start + size; auto addSegment = [&]( VECTOR2D aStart, VECTOR2D aEnd ) { std::unique_ptr shape = std::make_unique( aContainer, SHAPE_T::SEGMENT ); shape->SetStart( ScalePos( aStart ) ); shape->SetEnd( ScalePos( aEnd ) ); shape->SetFilled( aClosed ); shape->Rotate( ScalePos( start ), EDA_ANGLE( angle, DEGREES_T ) ); results.emplace_back( std::move( shape ) ); }; auto addArc = [&]( VECTOR2D aStart, VECTOR2D aEnd, VECTOR2D center ) { std::unique_ptr shape = std::make_unique( aContainer, SHAPE_T::ARC ); shape->SetStart( ScalePos( aStart ) ); shape->SetEnd( ScalePos( aEnd ) ); shape->SetCenter( ScalePos( center ) ); shape->SetFilled( aClosed ); shape->Rotate( ScalePos( start ), EDA_ANGLE( angle, DEGREES_T ) ); results.emplace_back( std::move( shape ) ); }; addSegment( { start.x + cr, start.y }, { end.x - cr, start.y } ); addSegment( { end.x, start.y - cr }, { end.x, end.y + cr } ); addSegment( { start.x + cr, end.y }, { end.x - cr, end.y } ); addSegment( { start.x, start.y - cr }, { start.x, end.y + cr } ); addArc( { end.x - cr, start.y }, { end.x, start.y - cr }, { end.x - cr, start.y - cr } ); addArc( { end.x, end.y + cr }, { end.x - cr, end.y }, { end.x - cr, end.y + cr } ); addArc( { start.x + cr, end.y }, { start.x, end.y + cr }, { start.x + cr, end.y + cr } ); addArc( { start.x, start.y - cr }, { start.x + cr, start.y }, { start.x + cr, start.y - cr } ); } } else if( str == wxS( "ARC" ) || str == wxS( "CARC" ) ) { VECTOR2D end; double angle = polyData.at( ++i ).get() / ( aInFill ? 10 : 1 ); end.x = ( polyData.at( ++i ) ); end.y = ( polyData.at( ++i ) ); std::unique_ptr shape = std::make_unique( aContainer, SHAPE_T::ARC ); if( angle < 0 ) { shape->SetStart( ScalePos( prevPt ) ); shape->SetEnd( ScalePos( end ) ); } else { shape->SetStart( ScalePos( end ) ); shape->SetEnd( ScalePos( prevPt ) ); } VECTOR2D delta = end - prevPt; VECTOR2D mid = ( prevPt + delta / 2 ); double ha = angle / 2; double hd = delta.EuclideanNorm() / 2; double cdist = hd / tan( DEG2RAD( ha ) ); VECTOR2D center = mid + delta.Perpendicular().Resize( cdist ); shape->SetCenter( ScalePos( center ) ); shape->SetFilled( aClosed ); results.emplace_back( std::move( shape ) ); prevPt = end; } else if( str == wxS( "L" ) ) { SHAPE_LINE_CHAIN chain; chain.Append( ScalePos( prevPt ) ); while( i < polyData.size() - 2 && polyData.at( i + 1 ).is_number() ) { VECTOR2D pt; pt.x = ( polyData.at( ++i ) ); pt.y = ( polyData.at( ++i ) ); chain.Append( ScalePos( pt ) ); prevPt = pt; } if( aClosed ) { std::unique_ptr shape = std::make_unique( aContainer, SHAPE_T::POLY ); wxASSERT( chain.PointCount() > 2 ); if( chain.PointCount() > 2 ) { chain.SetClosed( true ); shape->SetFilled( true ); shape->SetPolyShape( chain ); results.emplace_back( std::move( shape ) ); } } else { for( int s = 0; s < chain.SegmentCount(); s++ ) { SEG seg = chain.Segment( s ); std::unique_ptr shape = std::make_unique( aContainer, SHAPE_T::SEGMENT ); shape->SetStart( seg.A ); shape->SetEnd( seg.B ); results.emplace_back( std::move( shape ) ); } } } } else if( val.is_number() ) { prevPt.x = ( polyData.at( i ) ); prevPt.y = ( polyData.at( ++i ) ); } } return results; } SHAPE_LINE_CHAIN PCB_EASYEDAPRO_PARSER::ParseContour( nlohmann::json polyData, bool aInFill, double aArcAccuracy ) const { SHAPE_LINE_CHAIN result; VECTOR2D prevPt; double bezierMinSegLen = polyData.size() < 300 ? aArcAccuracy : aArcAccuracy * 10; for( int i = 0; i < polyData.size(); i++ ) { nlohmann::json val = polyData.at( i ); if( val.is_string() ) { wxString str = val; if( str == wxS( "CIRCLE" ) ) { VECTOR2D center; center.x = ( polyData.at( ++i ) ); center.y = ( polyData.at( ++i ) ); double r = ( polyData.at( ++i ) ); TransformCircleToPolygon( result, ScalePos( center ), ScaleSize( r ), ARC_HIGH_DEF, ERROR_INSIDE ); } else if( str == wxS( "R" ) ) { VECTOR2D start, size; start.x = ( polyData.at( ++i ) ); start.y = ( polyData.at( ++i ) ); size.x = ( polyData.at( ++i ) ); size.y = ( polyData.at( ++i ).get() ); double angle = polyData.at( ++i ); double cr = ( i + 1 ) < polyData.size() ? polyData.at( ++i ).get() : 0; SHAPE_POLY_SET poly; VECTOR2D kstart = ScalePos( start ); VECTOR2D ksize = ScaleSize( size ); VECTOR2D kcenter = kstart + ksize / 2; RotatePoint( kcenter, kstart, EDA_ANGLE( angle, DEGREES_T ) ); TransformRoundChamferedRectToPolygon( poly, kcenter, ksize, EDA_ANGLE( angle, DEGREES_T ), ScaleSize( cr ), 0, 0, 0, ARC_HIGH_DEF, ERROR_INSIDE ); result.Append( poly.Outline( 0 ) ); } else if( str == wxS( "ARC" ) || str == wxS( "CARC" ) ) { VECTOR2D end; double angle = polyData.at( ++i ).get(); if( aInFill ) // In .epcb fills, the angle is 10x for some reason angle /= 10; end.x = ( polyData.at( ++i ) ); end.y = ( polyData.at( ++i ) ); VECTOR2D arcStart, arcEnd; arcStart = prevPt; arcEnd = end; VECTOR2D delta = end - prevPt; VECTOR2D mid = ( prevPt + delta / 2 ); double ha = angle / 2; double hd = delta.EuclideanNorm() / 2; double cdist = hd / tan( DEG2RAD( ha ) ); VECTOR2D center = mid + delta.Perpendicular().Resize( cdist ); SHAPE_ARC sarc; sarc.ConstructFromStartEndCenter( ScalePos( arcStart ), ScalePos( arcEnd ), ScalePos( center ), angle >= 0, 0 ); result.Append( sarc, aArcAccuracy ); prevPt = end; } else if( str == wxS( "C" ) ) { VECTOR2D pt1; pt1.x = ( polyData.at( ++i ) ); pt1.y = ( polyData.at( ++i ) ); VECTOR2D pt2; pt2.x = ( polyData.at( ++i ) ); pt2.y = ( polyData.at( ++i ) ); VECTOR2D pt3; pt3.x = ( polyData.at( ++i ) ); pt3.y = ( polyData.at( ++i ) ); std::vector ctrlPoints = { ScalePos( prevPt ), ScalePos( pt1 ), ScalePos( pt2 ), ScalePos( pt3 ) }; BEZIER_POLY converter( ctrlPoints ); std::vector bezierPoints; converter.GetPoly( bezierPoints, bezierMinSegLen, 16 ); result.Append( bezierPoints ); prevPt = pt3; } else if( str == wxS( "L" ) ) { result.Append( ScalePos( prevPt ) ); while( i < polyData.size() - 2 && polyData.at( i + 1 ).is_number() ) { VECTOR2D pt; pt.x = ( polyData.at( ++i ) ); pt.y = ( polyData.at( ++i ) ); result.Append( ScalePos( pt ) ); prevPt = pt; } } } else if( val.is_number() ) { prevPt.x = ( polyData.at( i ) ); prevPt.y = ( polyData.at( ++i ) ); } } return result; } std::unique_ptr PCB_EASYEDAPRO_PARSER::createPAD( FOOTPRINT* aFootprint, const nlohmann::json& line ) { wxString uuid = line.at( 1 ); int unk = line.at( 2 ).get(); wxString netname = line.at( 3 ); int layer = line.at( 4 ).get(); PCB_LAYER_ID klayer = LayerToKi( layer ); wxString padNumber = line.at( 5 ); VECTOR2D center; center.x = line.at( 6 ); center.y = line.at( 7 ); double orientation = line.at( 8 ); nlohmann::json padHole = line.at( 9 ); nlohmann::json padShape = line.at( 10 ); std::unique_ptr pad = std::make_unique( aFootprint ); pad->SetNumber( padNumber ); pad->SetPosition( ScalePos( center ) ); pad->SetOrientationDegrees( orientation ); if( !padHole.is_null() ) { double drill_dir = 0; if( line.at( 14 ).is_number() ) drill_dir = line.at( 14 ); if( padHole.at( 0 ) == wxS( "ROUND" ) || padHole.at( 0 ) == wxS( "SLOT" ) ) { VECTOR2D drill; drill.x = padHole.at( 1 ); drill.y = padHole.at( 2 ); double deg = EDA_ANGLE( drill_dir, DEGREES_T ).Normalize90().AsDegrees(); if( std::abs( deg ) >= 45 ) std::swap( drill.x, drill.y ); // KiCad doesn't support arbitrary hole direction if( padHole.at( 0 ) == wxS( "SLOT" ) ) { pad->SetDrillShape( PAD_DRILL_SHAPE_OBLONG ); } pad->SetDrillSize( ScaleSize( drill ) ); pad->SetLayerSet( PAD::PTHMask() ); pad->SetAttribute( PAD_ATTRIB::PTH ); } } else { if( klayer == F_Cu ) { pad->SetLayerSet( PAD::SMDMask() ); } else if( klayer == B_Cu ) { pad->SetLayerSet( FlipLayerMask( PAD::SMDMask() ) ); } pad->SetAttribute( PAD_ATTRIB::SMD ); } wxString padSh = padShape.at( 0 ); if( padSh == wxS( "RECT" ) ) { VECTOR2D size; size.x = padShape.at( 1 ); size.y = padShape.at( 2 ); double cr_p = padShape.size() > 3 ? padShape.at( 3 ).get() : 0; pad->SetSize( ScaleSize( size ) ); if( cr_p == 0 ) { pad->SetShape( PAD_SHAPE::RECTANGLE ); } else { pad->SetShape( PAD_SHAPE::ROUNDRECT ); pad->SetRoundRectRadiusRatio( cr_p / 100 ); } } else if( padSh == wxS( "ELLIPSE" ) ) { VECTOR2D size; size.x = padShape.at( 1 ); size.y = padShape.at( 2 ); pad->SetSize( ScaleSize( size ) ); pad->SetShape( PAD_SHAPE::CIRCLE ); } else if( padSh == wxS( "OVAL" ) ) { VECTOR2D size; size.x = padShape.at( 1 ); size.y = padShape.at( 2 ); pad->SetSize( ScaleSize( size ) ); pad->SetShape( PAD_SHAPE::OVAL ); } else if( padSh == wxS( "POLY" ) ) { pad->SetShape( PAD_SHAPE::CUSTOM ); pad->SetAnchorPadShape( PAD_SHAPE::CIRCLE ); pad->SetSize( { 1, 1 } ); nlohmann::json polyData = padShape.at( 1 ); std::vector> results = ParsePoly( aFootprint, polyData, true, false ); for( auto& shape : results ) { shape->SetLayer( klayer ); shape->SetWidth( 0 ); shape->Move( -pad->GetPosition() ); pad->AddPrimitive( shape.release() ); } } pad->SetThermalSpokeAngle( ANGLE_90 ); return std::move( pad ); } FOOTPRINT* PCB_EASYEDAPRO_PARSER::ParseFootprint( const nlohmann::json& aProject, const wxString& aFpUuid, const std::vector& aLines ) { std::unique_ptr footprintPtr = std::make_unique( m_board ); FOOTPRINT* footprint = footprintPtr.get(); 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( const nlohmann::json& line : aLines ) { if( line.size() == 0 ) continue; wxString type = line.at( 0 ); if( type == wxS( "POLY" ) || type == wxS( "PAD" ) || type == wxS( "FILL" ) || type == wxS( "ATTR" ) ) { wxString uuid = line.at( 1 ); int unk = line.at( 2 ).get(); wxString netname = line.at( 3 ); int layer = line.at( 4 ).get(); PCB_LAYER_ID klayer = LayerToKi( layer ); if( type == wxS( "POLY" ) ) { double thickness = ( line.at( 5 ) ); nlohmann::json polyData = line.at( 6 ); std::vector> results = ParsePoly( footprint, polyData, false, false ); for( auto& shape : results ) { shape->SetLayer( klayer ); shape->SetWidth( ScaleSize( thickness ) ); footprint->Add( shape.release(), ADD_MODE::APPEND ); } } else if( type == wxS( "PAD" ) ) { std::unique_ptr pad = createPAD( footprint, line ); footprint->Add( pad.release(), ADD_MODE::APPEND ); } else if( type == wxS( "FILL" ) ) { int layer = line.at( 4 ).get(); PCB_LAYER_ID klayer = LayerToKi( layer ); double width = line.at( 5 ); nlohmann::json polyDataList = line.at( 7 ); if( !polyDataList.at( 0 ).is_array() ) polyDataList = nlohmann::json::array( { polyDataList } ); std::vector contours; for( nlohmann::json& polyData : polyDataList ) { SHAPE_LINE_CHAIN contour = ParseContour( polyData, false ); contour.SetClosed( true ); contours.push_back( contour ); } SHAPE_POLY_SET polySet; for( SHAPE_LINE_CHAIN& contour : contours ) polySet.AddOutline( contour ); polySet.RebuildHolesFromContours(); std::unique_ptr group; if( polySet.OutlineCount() > 1 ) group = std::make_unique( footprint ); BOX2I polyBBox = polySet.BBox(); for( const SHAPE_POLY_SET::POLYGON& poly : polySet.CPolygons() ) { std::unique_ptr shape = std::make_unique( footprint, SHAPE_T::POLY ); shape->SetFilled( true ); shape->SetPolyShape( poly ); shape->SetLayer( klayer ); shape->SetWidth( 0 ); if( group ) group->AddItem( shape.get() ); footprint->Add( shape.release(), ADD_MODE::APPEND ); } if( group ) footprint->Add( group.release(), ADD_MODE::APPEND ); } else if( type == wxS( "ATTR" ) ) { EASYEDAPRO::PCB_ATTR attr = line; if( attr.key == wxS( "Designator" ) ) footprint->GetField( REFERENCE_FIELD )->SetText( attr.value ); } } else if( type == wxS( "REGION" ) ) { wxString uuid = line.at( 1 ); int unk = line.at( 2 ).get(); int layer = line.at( 3 ).get(); PCB_LAYER_ID klayer = LayerToKi( layer ); double width = line.at( 4 ); std::set flags = line.at( 5 ); nlohmann::json polyDataList = line.at( 6 ); for( nlohmann::json& polyData : polyDataList ) { SHAPE_POLY_SET polySet; std::vector> results = ParsePoly( nullptr, polyData, true, false ); for( auto& shape : results ) { shape->SetFilled( true ); shape->TransformShapeToPolygon( polySet, klayer, 0, ARC_HIGH_DEF, ERROR_INSIDE, true ); } polySet.Simplify( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); std::unique_ptr zone = std::make_unique( footprint ); zone->SetIsRuleArea( true ); zone->SetDoNotAllowFootprints( !!flags.count( 2 ) ); zone->SetDoNotAllowCopperPour( !!flags.count( 7 ) || !!flags.count( 6 ) || !!flags.count( 8 ) ); zone->SetDoNotAllowPads( !!flags.count( 7 ) ); zone->SetDoNotAllowTracks( !!flags.count( 7 ) || !!flags.count( 5 ) ); zone->SetDoNotAllowVias( !!flags.count( 7 ) ); zone->SetLayer( klayer ); zone->Outline()->Append( polySet ); footprint->Add( zone.release(), ADD_MODE::APPEND ); } } } if( aProject.is_object() ) { std::map devicesMap = aProject.at( "devices" ); std::map compAttrs; for( auto& [devUuid, devData] : devicesMap ) { if( auto fp = get_opt( devData.attributes, "Footprint" ) ) { if( *fp == aFpUuid ) { compAttrs = devData.attributes; break; } } } wxString modelUuid, modelTitle, modelTransform; modelUuid = get_def( compAttrs, "3D Model", "" ); modelTitle = get_def( compAttrs, "3D Model Title", modelUuid ); modelTransform = get_def( compAttrs, "3D Model Transform", "" ); fillFootprintModelInfo( footprint, modelUuid, modelTitle, modelTransform ); } // Heal board outlines std::vector shapes; std::vector> newShapes; for( BOARD_ITEM* item : footprint->GraphicalItems() ) { if( !item->IsOnLayer( Edge_Cuts ) ) continue; if( item->Type() == PCB_SHAPE_T ) shapes.push_back( static_cast( item ) ); } ConnectBoardShapes( shapes, newShapes, SHAPE_JOIN_DISTANCE ); for( std::unique_ptr& ptr : newShapes ) footprint->Add( ptr.release(), ADD_MODE::APPEND ); return footprintPtr.release(); } void PCB_EASYEDAPRO_PARSER::ParseBoard( BOARD* aBoard, const nlohmann::json& aProject, std::map>& aFootprintMap, const std::map& aBlobMap, const std::multimap& aPouredMap, const std::vector& aLines, const wxString& aFpLibName ) { std::map> componentLines; std::map> ruleLines; BOARD_DESIGN_SETTINGS& bds = aBoard->GetDesignSettings(); for( const nlohmann::json& line : aLines ) { if( line.size() == 0 ) continue; wxString type = line.at( 0 ); if( type == wxS( "LAYER" ) ) { int layer = line.at( 1 ); PCB_LAYER_ID klayer = LayerToKi( layer ); wxString layerType = line.at( 2 ); wxString layerName = line.at( 3 ); int layerFlag = line.at( 4 ); if( layerFlag != 0 ) { LSET blayers = aBoard->GetEnabledLayers(); blayers.set( klayer ); aBoard->SetEnabledLayers( blayers ); aBoard->SetLayerName( klayer, layerName ); } } else if( type == wxS( "NET" ) ) { wxString netname = line.at( 1 ); aBoard->Add( new NETINFO_ITEM( aBoard, netname, aBoard->GetNetCount() + 1 ), ADD_MODE::APPEND ); } else if( type == wxS( "RULE" ) ) { wxString ruleType = line.at( 1 ); wxString ruleName = line.at( 2 ); int isDefault = line.at( 3 ); nlohmann::json ruleData = line.at( 4 ); if( ruleType == wxS( "3" ) && isDefault ) // Track width { wxString units = ruleData.at( 0 ); double minVal = ruleData.at( 1 ); double optVal = ruleData.at( 2 ); double maxVal = ruleData.at( 3 ); bds.m_TrackMinWidth = ScaleSize( minVal ); } else if( ruleType == wxS( "1" ) && isDefault ) { wxString units = ruleData.at( 0 ); nlohmann::json table = ruleData.at( 1 ); int minVal = INT_MAX; for( const std::vector& arr : table ) { for( int val : arr ) { if( val < minVal ) minVal = val; } } bds.m_MinClearance = ScaleSize( minVal ); } ruleLines[ruleType].push_back( line ); } else if( type == wxS( "VIA" ) || type == wxS( "LINE" ) || type == wxS( "ARC" ) || type == wxS( "POLY" ) || type == wxS( "FILL" ) || type == wxS( "POUR" ) ) { wxString uuid = line.at( 1 ); int unk = line.at( 2 ).get(); wxString netname = line.at( 3 ); if( type == wxS( "VIA" ) ) { VECTOR2D center; center.x = line.at( 5 ); center.y = line.at( 6 ); double drill = line.at( 7 ); double dia = line.at( 8 ); std::unique_ptr via = std::make_unique( aBoard ); via->SetPosition( ScalePos( center ) ); via->SetDrill( ScaleSize( drill ) ); via->SetWidth( ScaleSize( dia ) ); via->SetNet( aBoard->FindNet( netname ) ); aBoard->Add( via.release(), ADD_MODE::APPEND ); } else if( type == wxS( "LINE" ) ) { int layer = line.at( 4 ).get(); PCB_LAYER_ID klayer = LayerToKi( layer ); VECTOR2D start; start.x = line.at( 5 ); start.y = line.at( 6 ); VECTOR2D end; end.x = line.at( 7 ); end.y = line.at( 8 ); double width = line.at( 9 ); std::unique_ptr track = std::make_unique( aBoard ); track->SetLayer( klayer ); track->SetStart( ScalePos( start ) ); track->SetEnd( ScalePos( end ) ); track->SetWidth( ScaleSize( width ) ); track->SetNet( aBoard->FindNet( netname ) ); aBoard->Add( track.release(), ADD_MODE::APPEND ); } else if( type == wxS( "ARC" ) ) { int layer = line.at( 4 ).get(); PCB_LAYER_ID klayer = LayerToKi( layer ); VECTOR2D start; start.x = line.at( 5 ); start.y = line.at( 6 ); VECTOR2D end; end.x = line.at( 7 ); end.y = line.at( 8 ); double angle = line.at( 9 ); double width = line.at( 10 ); VECTOR2D delta = end - start; VECTOR2D mid = ( start + delta / 2 ); double ha = angle / 2; double hd = delta.EuclideanNorm() / 2; double cdist = hd / tan( DEG2RAD( ha ) ); VECTOR2D center = mid + delta.Perpendicular().Resize( cdist ); SHAPE_ARC sarc; sarc.ConstructFromStartEndCenter( ScalePos( start ), ScalePos( end ), ScalePos( center ), angle >= 0, width ); std::unique_ptr arc = std::make_unique( aBoard, &sarc ); arc->SetWidth( ScaleSize( width ) ); arc->SetLayer( klayer ); arc->SetNet( aBoard->FindNet( netname ) ); aBoard->Add( arc.release(), ADD_MODE::APPEND ); } else if( type == wxS( "FILL" ) ) { int layer = line.at( 4 ).get(); PCB_LAYER_ID klayer = LayerToKi( layer ); double width = line.at( 5 ); nlohmann::json polyDataList = line.at( 7 ); if( !polyDataList.at( 0 ).is_array() ) polyDataList = nlohmann::json::array( { polyDataList } ); std::vector contours; for( nlohmann::json& polyData : polyDataList ) { SHAPE_LINE_CHAIN contour = ParseContour( polyData, true ); contour.SetClosed( true ); contours.push_back( contour ); } SHAPE_POLY_SET zoneFillPoly; for( SHAPE_LINE_CHAIN& contour : contours ) zoneFillPoly.AddOutline( contour ); zoneFillPoly.RebuildHolesFromContours(); zoneFillPoly.Fracture( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); std::unique_ptr zone = std::make_unique( aBoard ); zone->SetNet( aBoard->FindNet( netname ) ); zone->SetLayer( klayer ); zone->Outline()->Append( SHAPE_RECT( zoneFillPoly.BBox() ).Outline() ); zone->SetFilledPolysList( klayer, zoneFillPoly ); zone->SetAssignedPriority( 500 ); zone->SetIsFilled( true ); zone->SetNeedRefill( false ); zone->SetLocalClearance( bds.m_MinClearance ); zone->SetMinThickness( bds.m_TrackMinWidth ); aBoard->Add( zone.release(), ADD_MODE::APPEND ); } else if( type == wxS( "POLY" ) ) { int layer = line.at( 4 ); PCB_LAYER_ID klayer = LayerToKi( layer ); double thickness = line.at( 5 ); nlohmann::json polyData = line.at( 6 ); std::vector> results = ParsePoly( aBoard, polyData, false, false ); for( auto& shape : results ) { shape->SetLayer( klayer ); shape->SetWidth( ScaleSize( thickness ) ); aBoard->Add( shape.release(), ADD_MODE::APPEND ); } } else if( type == wxS( "POUR" ) ) { int layer = line.at( 4 ).get(); PCB_LAYER_ID klayer = LayerToKi( layer ); double lineWidth = line.at( 5 ); // Doesn't matter wxString pourname = line.at( 6 ); int fillOrder = line.at( 7 ).get(); nlohmann::json polyDataList = line.at( 8 ); std::unique_ptr zone = std::make_unique( aBoard ); zone->SetNet( aBoard->FindNet( netname ) ); zone->SetLayer( klayer ); zone->SetAssignedPriority( 500 - fillOrder ); zone->SetLocalClearance( bds.m_MinClearance ); zone->SetMinThickness( bds.m_TrackMinWidth ); for( nlohmann::json& polyData : polyDataList ) { SHAPE_LINE_CHAIN contour = ParseContour( polyData, false ); contour.SetClosed( true ); zone->Outline()->Append( contour ); } wxASSERT( zone->Outline()->OutlineCount() == 1 ); SHAPE_POLY_SET fillPolySet; SHAPE_POLY_SET thermalSpokes; int entryId = 0; if( EASYEDAPRO::IMPORT_POURED ) { auto range = aPouredMap.equal_range( uuid ); for( auto& it = range.first; it != range.second; ++it ) { const EASYEDAPRO::POURED& poured = it->second; int unki = poured.unki; SHAPE_POLY_SET thisPoly; for( int dataId = 0; dataId < poured.polyData.size(); dataId++ ) { const nlohmann::json& fillData = poured.polyData[dataId]; const double ptScale = 10; SHAPE_LINE_CHAIN contour = ParseContour( fillData, false, ARC_HIGH_DEF / ptScale ); // Scale the fill for( int i = 0; i < contour.PointCount(); i++ ) contour.SetPoint( i, contour.GetPoint( i ) * ptScale ); if( poured.isPoly ) { contour.SetClosed( true ); // The contour can be self-intersecting SHAPE_POLY_SET simple( contour ); simple.Simplify( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); if( dataId == 0 ) { thisPoly.Append( simple ); } else { thisPoly.BooleanSubtract( simple, SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); } } else { const int thermalWidth = pcbIUScale.mmToIU( 0.2 ); // Generic for( int segId = 0; segId < contour.SegmentCount(); segId++ ) { const SEG& seg = contour.CSegment( segId ); TransformOvalToPolygon( thermalSpokes, seg.A, seg.B, thermalWidth, ARC_LOW_DEF, ERROR_INSIDE ); } } } fillPolySet.Append( thisPoly ); entryId++; } if( !fillPolySet.IsEmpty() ) { fillPolySet.Simplify( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); const int strokeWidth = pcbIUScale.MilsToIU( 8 ); // Seems to be 8 mils fillPolySet.Inflate( strokeWidth / 2, CORNER_STRATEGY::ROUND_ALL_CORNERS, ARC_HIGH_DEF, false ); fillPolySet.BooleanAdd( thermalSpokes, SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); fillPolySet.Fracture( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE ); zone->SetFilledPolysList( klayer, fillPolySet ); zone->SetNeedRefill( false ); zone->SetIsFilled( true ); } } aBoard->Add( zone.release(), ADD_MODE::APPEND ); } } else if( type == wxS( "TEARDROP" ) ) { wxString uuid = line.at( 1 ); wxString netname = line.at( 2 ); int layer = line.at( 3 ).get(); PCB_LAYER_ID klayer = LayerToKi( layer ); nlohmann::json polyData = line.at( 4 ); SHAPE_LINE_CHAIN contour = ParseContour( polyData, false ); contour.SetClosed( true ); std::unique_ptr zone = std::make_unique( aBoard ); zone->SetNet( aBoard->FindNet( netname ) ); zone->SetLayer( klayer ); zone->Outline()->Append( contour ); zone->SetFilledPolysList( klayer, contour ); zone->SetNeedRefill( false ); zone->SetIsFilled( true ); zone->SetAssignedPriority( 600 ); zone->SetLocalClearance( 0 ); zone->SetMinThickness( 0 ); zone->SetTeardropAreaType( TEARDROP_TYPE::TD_UNSPECIFIED ); aBoard->Add( zone.release(), ADD_MODE::APPEND ); } else if( type == wxS( "REGION" ) ) { wxString uuid = line.at( 1 ); int unk = line.at( 2 ).get(); int layer = line.at( 3 ).get(); PCB_LAYER_ID klayer = LayerToKi( layer ); double width = line.at( 4 ); std::set flags = line.at( 5 ); nlohmann::json polyDataList = line.at( 6 ); for( nlohmann::json& polyData : polyDataList ) { SHAPE_LINE_CHAIN contour = ParseContour( polyData, false ); contour.SetClosed( true ); std::unique_ptr zone = std::make_unique( aBoard ); zone->SetIsRuleArea( true ); zone->SetDoNotAllowFootprints( !!flags.count( 2 ) ); zone->SetDoNotAllowCopperPour( !!flags.count( 7 ) || !!flags.count( 6 ) || !!flags.count( 8 ) ); zone->SetDoNotAllowPads( !!flags.count( 7 ) ); zone->SetDoNotAllowTracks( !!flags.count( 7 ) || !!flags.count( 5 ) ); zone->SetDoNotAllowVias( !!flags.count( 7 ) ); zone->SetLayer( klayer ); zone->Outline()->Append( contour ); aBoard->Add( zone.release(), ADD_MODE::APPEND ); } } else if( type == wxS( "PAD" ) ) { wxString netname = line.at( 3 ); std::unique_ptr footprint = std::make_unique( aBoard ); std::unique_ptr pad = createPAD( footprint.get(), line ); pad->SetNet( aBoard->FindNet( netname ) ); VECTOR2I pos = pad->GetPosition(); EDA_ANGLE orient = pad->GetOrientation(); pad->SetPosition( VECTOR2I() ); pad->SetOrientation( ANGLE_0 ); footprint->Add( pad.release(), ADD_MODE::APPEND ); footprint->SetPosition( pos ); footprint->SetOrientation( orient ); wxString fpName = wxS( "Pad_" ) + line.at( 1 ).get(); LIB_ID fpID = EASYEDAPRO::ToKiCadLibID( wxEmptyString, fpName ); footprint->SetFPID( fpID ); footprint->Reference().SetVisible( true ); footprint->Value().SetVisible( true ); footprint->AutoPositionFields(); aBoard->Add( footprint.release(), ADD_MODE::APPEND ); } else if( type == wxS( "IMAGE" ) ) { wxString uuid = line.at( 1 ); int unk = line.at( 2 ).get(); int layer = line.at( 3 ).get(); PCB_LAYER_ID klayer = LayerToKi( layer ); VECTOR2D start( line.at( 4 ), line.at( 5 ) ); VECTOR2D size( line.at( 6 ), line.at( 7 ) ); double angle = line.at( 8 ); // from top left corner int mirror = line.at( 9 ); nlohmann::json polyDataList = line.at( 10 ); BOX2I bbox; std::vector contours; for( nlohmann::json& polyData : polyDataList ) { SHAPE_LINE_CHAIN contour = ParseContour( polyData, false ); contour.SetClosed( true ); contours.push_back( contour ); bbox.Merge( contour.BBox() ); } VECTOR2D scale( ScaleSize( size.x ) / bbox.GetSize().x, ScaleSize( size.y ) / bbox.GetSize().y ); SHAPE_POLY_SET polySet; for( SHAPE_LINE_CHAIN& contour : contours ) { for( int i = 0; i < contour.PointCount(); i++ ) { VECTOR2I pt = contour.CPoint( i ); contour.SetPoint( i, VECTOR2I( pt.x * scale.x, pt.y * scale.y ) ); } polySet.AddOutline( contour ); } polySet.RebuildHolesFromContours(); std::unique_ptr group; if( polySet.OutlineCount() > 1 ) group = std::make_unique( aBoard ); BOX2I polyBBox = polySet.BBox(); for( const SHAPE_POLY_SET::POLYGON& poly : polySet.CPolygons() ) { std::unique_ptr shape = std::make_unique( aBoard, SHAPE_T::POLY ); shape->SetFilled( true ); shape->SetPolyShape( poly ); shape->SetLayer( klayer ); shape->SetWidth( 0 ); shape->Move( ScalePos( start ) - polyBBox.GetOrigin() ); shape->Rotate( ScalePos( start ), EDA_ANGLE( angle, DEGREES_T ) ); if( IsBackLayer( klayer ) ^ !!mirror ) shape->Mirror( ScalePos( start ), !IsBackLayer( klayer ) ); if( group ) group->AddItem( shape.get() ); aBoard->Add( shape.release(), ADD_MODE::APPEND ); } if( group ) aBoard->Add( group.release(), ADD_MODE::APPEND ); } else if( type == wxS( "STRING" ) ) { wxString uuid = line.at( 1 ); int unk = line.at( 2 ); int layer = line.at( 3 ).get(); PCB_LAYER_ID klayer = LayerToKi( layer ); VECTOR2D location( line.at( 4 ), line.at( 5 ) ); wxString string = line.at( 6 ); wxString font = line.at( 7 ); double height = line.at( 8 ); double strokew = line.at( 9 ); int align = line.at( 12 ); double angle = line.at( 13 ); int inverted = line.at( 14 ); int mirror = line.at( 16 ); PCB_TEXT* text = new PCB_TEXT( aBoard ); text->SetText( string ); text->SetLayer( klayer ); text->SetPosition( ScalePos( location ) ); text->SetIsKnockout( inverted ); text->SetTextThickness( ScaleSize( strokew ) ); text->SetTextSize( VECTOR2D( ScaleSize( height * 0.6 ), ScaleSize( height * 0.7 ) ) ); if( font != wxS( "default" ) ) { text->SetFont( KIFONT::FONT::GetFont( font ) ); //text->SetupRenderCache( text->GetShownText(), EDA_ANGLE( angle, DEGREES_T ) ); //text->AddRenderCacheGlyph(); // TODO: import geometry cache } AlignText( text, align ); if( IsBackLayer( klayer ) ^ !!mirror ) { text->SetMirrored( true ); text->SetTextAngleDegrees( -angle ); } else { text->SetTextAngleDegrees( angle ); } aBoard->Add( text, ADD_MODE::APPEND ); } else if( type == wxS( "COMPONENT" ) ) { wxString compId = line.at( 1 ); componentLines[compId].push_back( line ); } else if( type == wxS( "ATTR" ) ) { wxString compId = line.at( 3 ); componentLines[compId].push_back( line ); } else if( type == wxS( "PAD_NET" ) ) { wxString compId = line.at( 1 ); componentLines[compId].push_back( line ); } } for( auto const& [compId, lines] : componentLines ) { wxString deviceId; wxString fpIdOverride; wxString fpDesignator; std::map localCompAttribs; for( auto& line : lines ) { if( line.size() == 0 ) continue; wxString type = line.at( 0 ); if( type == wxS( "COMPONENT" ) ) { localCompAttribs = line.at( 7 ); } else if( type == wxS( "ATTR" ) ) { EASYEDAPRO::PCB_ATTR attr = line; if( attr.key == wxS( "Device" ) ) deviceId = attr.value; else if( attr.key == wxS( "Footprint" ) ) fpIdOverride = attr.value; else if( attr.key == wxS( "Designator" ) ) fpDesignator = attr.value; } } if( deviceId.empty() ) continue; nlohmann::json compAttrs = aProject.at( "devices" ).at( deviceId ).at( "attributes" ); wxString fpId; if( !fpIdOverride.IsEmpty() ) fpId = fpIdOverride; else fpId = compAttrs.at( "Footprint" ).get(); auto it = aFootprintMap.find( fpId ); if( it == aFootprintMap.end() ) { wxLogError( "Footprint of '%s' with uuid '%s' not found.", fpDesignator, fpId ); continue; } std::unique_ptr& footprintOrig = it->second; std::unique_ptr footprint( static_cast( footprintOrig->Clone() ) ); wxString modelUuid, modelTitle, modelTransform; if( auto val = get_opt( localCompAttribs, "3D Model" ) ) modelUuid = *val; else modelUuid = compAttrs.value( "3D Model", "" ); if( auto val = get_opt( localCompAttribs, "3D Model Title" ) ) modelTitle = val->Trim(); else modelTitle = compAttrs.value( "3D Model Title", modelUuid ).Trim(); if( auto val = get_opt( localCompAttribs, "3D Model Transform" ) ) modelTransform = *val; else modelTransform = compAttrs.value( "3D Model Transform", "" ); fillFootprintModelInfo( footprint.get(), modelUuid, modelTitle, modelTransform ); footprint->SetParent( aBoard ); for( auto& line : lines ) { if( line.size() == 0 ) continue; wxString type = line.at( 0 ); if( type == wxS( "COMPONENT" ) ) { int layer = line.at( 3 ); PCB_LAYER_ID klayer = LayerToKi( layer ); VECTOR2D center( line.at( 4 ), line.at( 5 ) ); double orient = line.at( 6 ); //std::map props = line.at( 7 ); if( klayer == B_Cu ) footprint->Flip( footprint->GetPosition(), false ); footprint->SetOrientationDegrees( orient ); footprint->SetPosition( ScalePos( center ) ); } else if( type == wxS( "ATTR" ) ) { EASYEDAPRO::PCB_ATTR attr = line; PCB_LAYER_ID klayer = LayerToKi( attr.layer ); PCB_TEXT* text = nullptr; bool add = false; if( attr.key == wxS( "Designator" ) ) { if( attr.key == wxS( "Designator" ) ) { text = footprint->GetField( REFERENCE_FIELD ); } else { text = new PCB_TEXT( footprint.get() ); add = true; } if( attr.fontName != wxS( "default" ) ) text->SetFont( KIFONT::FONT::GetFont( attr.fontName ) ); if( attr.valVisible && attr.keyVisible ) { text->SetText( attr.key + ':' + attr.value ); } else if( attr.keyVisible ) { text->SetText( attr.key ); } else { text->SetText( attr.value ); } text->SetVisible( attr.keyVisible || attr.valVisible ); text->SetLayer( klayer ); text->SetPosition( ScalePos( attr.position ) ); text->SetTextAngleDegrees( footprint->IsFlipped() ? -attr.rotation : attr.rotation ); text->SetIsKnockout( attr.inverted ); text->SetTextThickness( ScaleSize( attr.strokeWidth ) ); text->SetTextSize( VECTOR2D( ScaleSize( attr.height * 0.55 ), ScaleSize( attr.height * 0.6 ) ) ); AlignText( text, attr.textOrigin ); if( add ) footprint->Add( text, ADD_MODE::APPEND ); } } else if( type == wxS( "PAD_NET" ) ) { wxString padNumber = line.at( 2 ); wxString padNet = line.at( 3 ); PAD* pad = footprint->FindPadByNumber( padNumber ); if( pad ) { pad->SetNet( aBoard->FindNet( padNet ) ); } else { // Not a pad } } } aBoard->Add( footprint.release(), ADD_MODE::APPEND ); } // Heal board outlines std::vector shapes; std::vector> newShapes; for( BOARD_ITEM* item : aBoard->Drawings() ) { if( !item->IsOnLayer( Edge_Cuts ) ) continue; if( item->Type() == PCB_SHAPE_T ) shapes.push_back( static_cast( item ) ); } ConnectBoardShapes( shapes, newShapes, SHAPE_JOIN_DISTANCE ); for( std::unique_ptr& ptr : newShapes ) aBoard->Add( ptr.release(), ADD_MODE::APPEND ); // Center the board BOX2I outlineBbox = aBoard->ComputeBoundingBox( true ); PAGE_INFO pageInfo = aBoard->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; aBoard->Move( offset ); bds.SetAuxOrigin( offset ); }