kicad/pcbnew/exporters/step/exporter_step.cpp

580 lines
19 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2022 Mark Roszko <mark.roszko@gmail.com>
* Copyright (C) 2016 Cirilo Bernardo <cirilo.bernardo@gmail.com>
* 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 <board.h>
#include <board_design_settings.h>
#include <footprint.h>
#include <pcb_track.h>
#include <pcb_shape.h>
#include <pad.h>
#include <zone.h>
#include <fp_lib_table.h>
#include "step_pcb_model.h"
#include <pgm_base.h>
#include <base_units.h>
#include <filename_resolver.h>
#include <trace_helpers.h>
#include <project_pcb.h>
#include <Message.hxx> // OpenCascade messenger
#include <Message_PrinterOStream.hxx> // OpenCascade output messenger
#include <Standard_Failure.hxx> // In open cascade
#include <Standard_Version.hxx>
#include <wx/crt.h>
#include <wx/log.h>
#include <core/profile.h> // To use GetRunningMicroSecs or another profiling utility
#define OCC_VERSION_MIN 0x070500
#if OCC_VERSION_HEX < OCC_VERSION_MIN
#include <Message_Messenger.hxx>
#endif
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;
};
wxString EXPORTER_STEP_PARAMS::GetDefaultExportExtension()
{
switch( m_format )
{
case EXPORTER_STEP_PARAMS::FORMAT::STEP: return wxS( "step" ); break;
case EXPORTER_STEP_PARAMS::FORMAT::GLB: return wxS( "glb" ); break;
default: return wxEmptyString; // shouldn't happen
}
}
wxString EXPORTER_STEP_PARAMS::GetFormatName()
{
switch( m_format )
{
// honestly these names shouldn't be translated since they are mostly industry standard acronyms
case EXPORTER_STEP_PARAMS::FORMAT::STEP: return wxS( "STEP" ); break;
case EXPORTER_STEP_PARAMS::FORMAT::GLB: return wxS("Binary GLTF" ); break;
default: return wxEmptyString; // shouldn't happen
}
}
EXPORTER_STEP::EXPORTER_STEP( BOARD* aBoard, const EXPORTER_STEP_PARAMS& aParams ) :
m_params( aParams ),
m_error( false ),
m_fail( false ),
m_warn( false ),
m_board( aBoard ),
m_pcbModel( nullptr ),
m_boardThickness( DEFAULT_BOARD_THICKNESS_MM )
{
m_solderMaskColor = COLOR4D( 0.08, 0.20, 0.14, 0.83 );
m_copperColor = COLOR4D( 0.7, 0.61, 0.0, 1.0 );
// 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<FILENAME_RESOLVER>();
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::buildFootprint3DShapes( 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( ExportTracksAndVias() )
{
if( m_pcbModel->AddPadShape( pad, aOrigin ) )
hasdata = true;
}
}
// Build 3D shapes of the footprint graphic items on external layers:
if( ExportTracksAndVias() )
{
int maxError = m_board->GetDesignSettings().m_MaxError;
aFootprint->TransformFPShapesToPolySet( m_top_copper_shapes, F_Cu, 0, maxError, ERROR_INSIDE,
false, /* include text */
true, /* include shapes */
false /* include private items */ );
aFootprint->TransformFPShapesToPolySet( m_bottom_copper_shapes, B_Cu, 0, maxError, ERROR_INSIDE,
false, /* include text */
true, /* include shapes */
false /* include private items */ );
}
if( ( !(aFootprint->GetAttributes() & (FP_THROUGH_HOLE|FP_SMD)) ) && !m_params.m_includeUnspecified )
{
return hasdata;
}
if( ( aFootprint->GetAttributes() & FP_DNP ) && !m_params.m_includeDNP )
{
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 =
PROJECT_PCB::PcbFootprintLibs( m_board->GetProject() )->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<wxString> 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::buildTrack3DShape( PCB_TRACK* aTrack, VECTOR2D aOrigin )
{
if( aTrack->Type() == PCB_VIA_T )
{
return m_pcbModel->AddViaShape( static_cast<const PCB_VIA*>( aTrack ), aOrigin );
}
PCB_LAYER_ID pcblayer = aTrack->GetLayer();
if( pcblayer != F_Cu && pcblayer != B_Cu )
return false;
if( aTrack->Type() == PCB_ARC_T )
{
int maxError = m_board->GetDesignSettings().m_MaxError;
if( pcblayer == F_Cu )
aTrack->TransformShapeToPolygon( m_top_copper_shapes, pcblayer, 0, maxError, ERROR_INSIDE );
else
aTrack->TransformShapeToPolygon( m_bottom_copper_shapes, pcblayer, 0, maxError, ERROR_INSIDE );
}
else
m_pcbModel->AddTrackSegment( aTrack, aOrigin );
return true;
}
void EXPORTER_STEP::buildZones3DShape( VECTOR2D aOrigin )
{
for( ZONE* zone : m_board->Zones() )
{
for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
{
if( layer == F_Cu || layer == B_Cu )
{
SHAPE_POLY_SET copper_shape;
zone->TransformSolidAreasShapesToPolygon( layer, copper_shape );
copper_shape.Unfracture( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE );
m_pcbModel->AddCopperPolygonShapes( &copper_shape, layer == F_Cu, aOrigin, false );
}
}
}
}
bool EXPORTER_STEP::buildGraphic3DShape( BOARD_ITEM* aItem, VECTOR2D aOrigin )
{
PCB_SHAPE* graphic = dynamic_cast<PCB_SHAPE*>( aItem );
if( ! graphic )
return false;
PCB_LAYER_ID pcblayer = graphic->GetLayer();
if( pcblayer != F_Cu && pcblayer != B_Cu )
return false;
SHAPE_POLY_SET copper_shapes;
int maxError = m_board->GetDesignSettings().m_MaxError;
if( pcblayer == F_Cu )
graphic->TransformShapeToPolygon( m_top_copper_shapes, pcblayer, 0,
maxError, ERROR_INSIDE );
else
graphic->TransformShapeToPolygon( m_bottom_copper_shapes, pcblayer, 0,
maxError, ERROR_INSIDE );
return true;
}
bool EXPORTER_STEP::buildBoard3DShapes()
{
if( m_pcbModel )
return true;
SHAPE_POLY_SET pcbOutlines; // stores the board main outlines
if( !m_board->GetBoardPolygonOutlines( pcbOutlines,
/* error handler */ nullptr,
/* allows use arcs in outlines */ true ) )
{
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<STEP_PCB_MODEL>( 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->SetCopperColor( m_copperColor.r, m_copperColor.g, m_copperColor.b );
m_pcbModel->SetPCBThickness( m_boardThickness );
// Note: m_params.m_BoardOutlinesChainingEpsilon is used only to build the board outlines,
// not to set OCC chaining epsilon (much smaller)
//
// Set the min distance between 2 points for OCC to see these 2 points as merged
// OCC_MAX_DISTANCE_TO_MERGE_POINTS is acceptable for OCC, otherwise there are issues
// to handle the shapes chaining on copper layers, because the Z dist is 0.035 mm and the
// min dist must be much smaller (we use 0.001 mm giving good results)
m_pcbModel->OCCSetMergeMaxDistance( OCC_MAX_DISTANCE_TO_MERGE_POINTS );
m_pcbModel->SetMaxError( m_board->GetDesignSettings().m_MaxError );
// For copper layers, only pads and tracks are added, because adding everything on copper
// generate unreasonable file sizes and take a unreasonable calculation time.
for( FOOTPRINT* fp : m_board->Footprints() )
buildFootprint3DShapes( fp, origin );
if( ExportTracksAndVias() )
{
for( PCB_TRACK* track : m_board->Tracks() )
buildTrack3DShape( track, origin );
for( BOARD_ITEM* item : m_board->Drawings() )
buildGraphic3DShape( item, origin );
}
m_pcbModel->AddCopperPolygonShapes( &m_top_copper_shapes, true, origin, true );
m_pcbModel->AddCopperPolygonShapes( &m_bottom_copper_shapes, false, origin, true );
if( m_params.m_exportZones )
{
buildZones3DShape( 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::calculatePcbThickness()
{
m_boardThickness = DEFAULT_BOARD_THICKNESS_MM;
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:
case BS_ITEM_TYPE_SOLDERMASK:
if( item->IsEnabled() )
thickness += item->GetThickness();
break;
default:
break;
}
}
if( thickness > 0 )
m_boardThickness = pcbIUScale.IUTomm( thickness );
}
}
bool EXPORTER_STEP::Export()
{
// Display the export time, for statistics
unsigned stats_startExportTime = GetRunningMicroSecs();
// setup opencascade message log
Message::DefaultMessenger()->RemovePrinters( STANDARD_TYPE( Message_PrinterOStream ) );
Message::DefaultMessenger()->AddPrinter( new KiCadPrinter( this ) );
ReportMessage( _( "Determining PCB data\n" ) );
calculatePcbThickness();
wxString msg;
msg.Printf( _( "Board Thickness from stackup: %.3f mm\n" ), m_boardThickness );
ReportMessage( msg );
if( m_params.m_outputFile.IsEmpty() )
{
wxFileName fn = m_board->GetFileName();
fn.SetName( fn.GetName() );
fn.SetExt( m_params.GetDefaultExportExtension() );
m_params.m_outputFile = fn.GetFullName();
}
try
{
ReportMessage( wxString::Format( _( "Build %s data\n" ), m_params.GetFormatName() ) );
if( !buildBoard3DShapes() )
{
ReportMessage( _( "\n** Error building STEP board model. Export aborted. **\n" ) );
return false;
}
ReportMessage( wxString::Format( _( "Writing %s file\n" ), m_params.GetFormatName() ) );
bool success = true;
if( m_params.m_format == EXPORTER_STEP_PARAMS::FORMAT::STEP )
success = m_pcbModel->WriteSTEP( m_outputFile, m_params.m_optimizeStep );
else if( m_params.m_format == EXPORTER_STEP_PARAMS::FORMAT::GLB )
success = m_pcbModel->WriteGLTF( m_outputFile );
if( !success )
{
ReportMessage( wxString::Format( _( "\n** Error writing %s file. **\n" ),
m_params.GetFormatName() ) );
return false;
}
else
{
ReportMessage( wxString::Format( _( "%s file '%s' created.\n" ),
m_params.GetFormatName(), m_outputFile ) );
}
}
catch( const Standard_Failure& e )
{
ReportMessage( e.GetMessageString() );
ReportMessage( wxString::Format( _( "\n** Error exporting %s file. Export aborted. **\n" ),
m_params.GetFormatName() ) );
return false;
}
catch( ... )
{
ReportMessage( wxString::Format( _( "\n** Error exporting %s file. Export aborted. **\n" ),
m_params.GetFormatName() ) );
return false;
}
if( m_fail || m_error )
{
if( m_fail )
{
msg = wxString::Format( _( "Unable to create %s file.\n"
"Check that the board has a valid outline and models." ),
m_params.GetFormatName() );
}
else if( m_error || m_warn )
{
msg = wxString::Format( _( "%s file has been created, but there are warnings." ),
m_params.GetFormatName() );
}
ReportMessage( msg );
}
// Display calculation time in seconds
double calculation_time = (double)( GetRunningMicroSecs() - stats_startExportTime) / 1e6;
ReportMessage( wxString::Format( _( "\nExport time %.3f s\n" ), calculation_time ) );
return true;
}