/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2022 Mark Roszko * Copyright (C) 2016 Cirilo Bernardo * Copyright (C) 2016-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 "exporter_step.h" #include #include #include #include #include #include #include #include #include // OpenCascade messenger #include // OpenCascade output messenger #include // In open cascade #include #include #define OCC_VERSION_MIN 0x070500 #if OCC_VERSION_HEX < OCC_VERSION_MIN #include #endif #define DEFAULT_BOARD_THICKNESS 1.6 void ReportMessage( const wxString& aMessage ) { wxPrintf( aMessage ); fflush( stdout ); // Force immediate printing (needed on mingw) } class KiCadPrinter : public Message_Printer { public: KiCadPrinter( EXPORTER_STEP* aConverter ) : m_converter( aConverter ) {} protected: #if OCC_VERSION_HEX < OCC_VERSION_MIN virtual void Send( const TCollection_ExtendedString& theString, const Message_Gravity theGravity, const Standard_Boolean theToPutEol ) const override { Send( TCollection_AsciiString( theString ), theGravity, theToPutEol ); } virtual void Send( const TCollection_AsciiString& theString, const Message_Gravity theGravity, const Standard_Boolean theToPutEol ) const override #else virtual void send( const TCollection_AsciiString& theString, const Message_Gravity theGravity ) const override #endif { if( theGravity >= Message_Warning || ( wxLog::IsAllowedTraceMask( traceKiCad2Step ) && theGravity == Message_Info ) ) { ReportMessage( theString.ToCString() ); #if OCC_VERSION_HEX < OCC_VERSION_MIN if( theToPutEol ) ReportMessage( wxT( "\n" ) ); #else ReportMessage( wxT( "\n" ) ); #endif } if( theGravity == Message_Warning ) m_converter->SetWarn(); if( theGravity >= Message_Alarm ) m_converter->SetError(); if( theGravity == Message_Fail ) m_converter->SetFail(); } private: EXPORTER_STEP* m_converter; }; EXPORTER_STEP::EXPORTER_STEP( BOARD* aBoard, const EXPORTER_STEP_PARAMS& aParams ) : m_params( aParams ), m_error( false ), m_fail( false ), m_warn( false ), m_hasDrillOrigin( false ), m_hasGridOrigin( false ), m_board( aBoard ), m_pcbModel( nullptr ), m_minDistance( STEPEXPORT_MIN_DISTANCE ), m_boardThickness( DEFAULT_BOARD_THICKNESS ) { m_solderMaskColor = COLOR4D( 0.08, 0.20, 0.14, 0.83 ); // Init m_pcbBaseName to the board short filename (no path, no ext) // m_pcbName is used later to identify items in step file wxFileName fn( aBoard->GetFileName() ); m_pcbBaseName = fn.GetName(); m_resolver = std::make_unique(); m_resolver->Set3DConfigDir( wxT( "" ) ); // needed to add the project to the search stack m_resolver->SetProject( aBoard->GetProject() ); m_resolver->SetProgramBase( &Pgm() ); } EXPORTER_STEP::~EXPORTER_STEP() { } bool EXPORTER_STEP::composePCB( FOOTPRINT* aFootprint, VECTOR2D aOrigin ) { bool hasdata = false; // Dump the pad holes into the PCB for( PAD* pad : aFootprint->Pads() ) { if( m_pcbModel->AddPadHole( pad, aOrigin ) ) hasdata = true; } if( ( aFootprint->GetAttributes() & FP_EXCLUDE_FROM_BOM ) && !m_params.m_includeExcludedBom ) { return hasdata; } // Prefetch the library for this footprint // In case we need to resolve relative footprint paths wxString libraryName = aFootprint->GetFPID().GetLibNickname(); wxString footprintBasePath = wxEmptyString; double posX = aFootprint->GetPosition().x - aOrigin.x; double posY = (aFootprint->GetPosition().y) - aOrigin.y; if( m_board->GetProject() ) { try { // FindRow() can throw an exception const FP_LIB_TABLE_ROW* fpRow = m_board->GetProject()->PcbFootprintLibs()->FindRow( libraryName, false ); if( fpRow ) footprintBasePath = fpRow->GetFullURI( true ); } catch( ... ) { // Do nothing if the libraryName is not found in lib table } } // Exit early if we don't want to include footprint models if( m_params.m_boardOnly ) { return hasdata; } VECTOR2D newpos( pcbIUScale.IUTomm( posX ), pcbIUScale.IUTomm( posY ) ); for( const FP_3DMODEL& fp_model : aFootprint->Models() ) { if( !fp_model.m_Show || fp_model.m_Filename.empty() ) continue; std::vector searchedPaths; wxString mname = m_resolver->ResolvePath( fp_model.m_Filename, footprintBasePath ); if( !wxFileName::FileExists( mname ) ) { ReportMessage( wxString::Format( wxT( "Could not add 3D model to %s.\n" "File not found: %s\n" ), aFootprint->GetReference(), mname ) ); continue; } std::string fname( mname.ToUTF8() ); std::string refName( aFootprint->GetReference().ToUTF8() ); try { bool bottomSide = aFootprint->GetLayer() == B_Cu; // the rotation is stored in degrees but opencascade wants radians VECTOR3D modelRot = fp_model.m_Rotation; modelRot *= M_PI; modelRot /= 180.0; if( m_pcbModel->AddComponent( fname, refName, bottomSide, newpos, aFootprint->GetOrientation().AsRadians(), fp_model.m_Offset, modelRot, fp_model.m_Scale, m_params.m_substModels ) ) { hasdata = true; } } catch( const Standard_Failure& e ) { ReportMessage( wxString::Format( wxT( "Could not add 3D model to %s.\n" "OpenCASCADE error: %s\n" ), aFootprint->GetReference(), e.GetMessageString() ) ); } } return hasdata; } bool EXPORTER_STEP::composePCB() { if( m_pcbModel ) return true; SHAPE_POLY_SET pcbOutlines; // stores the board main outlines if( !m_board->GetBoardPolygonOutlines( pcbOutlines ) ) { wxLogWarning( _( "Board outline is malformed. Run DRC for a full analysis." ) ); } VECTOR2D origin; // Determine the coordinate system reference: // Precedence of reference point is Drill Origin > Grid Origin > User Offset if( m_params.m_useDrillOrigin ) origin = m_board->GetDesignSettings().GetAuxOrigin(); else if( m_params.m_useGridOrigin ) origin = m_board->GetDesignSettings().GetGridOrigin(); else origin = m_params.m_origin; m_pcbModel = std::make_unique( m_pcbBaseName ); // TODO: Handle when top & bottom soldermask colours are different... m_pcbModel->SetBoardColor( m_solderMaskColor.r, m_solderMaskColor.g, m_solderMaskColor.b ); m_pcbModel->SetPCBThickness( m_boardThickness ); // Set the min distance betewenn 2 points for OCC to see these 2 points as merged double minDistmm = std::max( m_params.m_minDistance, STEPEXPORT_MIN_ACCEPTABLE_DISTANCE ); m_pcbModel->SetMinDistance( minDistmm ); m_pcbModel->SetMaxError( m_board->GetDesignSettings().m_MaxError ); for( FOOTPRINT* i : m_board->Footprints() ) composePCB( i, origin ); ReportMessage( wxT( "Create PCB solid model\n" ) ); wxString msg; msg.Printf( wxT( "Board outline: find %d initial points\n" ), pcbOutlines.FullPointCount() ); ReportMessage( msg ); if( !m_pcbModel->CreatePCB( pcbOutlines, origin ) ) { ReportMessage( wxT( "could not create PCB solid model\n" ) ); return false; } return true; } void EXPORTER_STEP::determinePcbThickness() { m_boardThickness = DEFAULT_BOARD_THICKNESS; const BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings(); if( bds.GetStackupDescriptor().GetCount() ) { int thickness = 0; for( BOARD_STACKUP_ITEM* item : bds.GetStackupDescriptor().GetList() ) { switch( item->GetType() ) { case BS_ITEM_TYPE_DIELECTRIC: // Dielectric can have sub-layers. Layer 0 is the main layer // Not frequent, but possible for( int idx = 0; idx < item->GetSublayersCount(); idx++ ) thickness += item->GetThickness( idx ); break; case BS_ITEM_TYPE_COPPER: if( item->IsEnabled() ) thickness += item->GetThickness(); break; default: break; } } if( thickness > 0 ) m_boardThickness = pcbIUScale.IUTomm( thickness ); } } bool EXPORTER_STEP::Export() { // setup opencascade message log Message::DefaultMessenger()->RemovePrinters( STANDARD_TYPE( Message_PrinterOStream ) ); Message::DefaultMessenger()->AddPrinter( new KiCadPrinter( this ) ); ReportMessage( _( "Determining PCB data\n" ) ); determinePcbThickness(); wxString msg; msg.Printf( _( "Board Thickness from stackup: %.3f mm\n" ), m_boardThickness ); ReportMessage( msg ); try { ReportMessage( _( "Build STEP data\n" ) ); if( !composePCB() ) { ReportMessage( _( "\n** Error building STEP board model. Export aborted. **\n" ) ); return false; } ReportMessage( _( "Writing STEP file\n" ) ); if( !m_pcbModel->WriteSTEP( m_outputFile ) ) { ReportMessage( _( "\n** Error writing STEP file. **\n" ) ); return false; } else { ReportMessage( wxString::Format( _( "\nSTEP file '%s' created.\n" ), m_outputFile ) ); } } catch( const Standard_Failure& e ) { ReportMessage( e.GetMessageString() ); ReportMessage( _( "\n** Error exporting STEP file. Export aborted. **\n" ) ); return false; } catch( ... ) { ReportMessage( _( "\n** Error exporting STEP file. Export aborted. **\n" ) ); return false; } if( m_fail || m_error ) { if( m_fail ) { msg = _( "Unable to create STEP file.\n" "Check that the board has a valid outline and models." ); } else if( m_error || m_warn ) { msg = _( "STEP file has been created, but there are warnings." ); } ReportMessage( msg ); } return true; }