Add binary GLTF export option to kicad-cli

Not by any means finished
This commit is contained in:
Marek Roszko 2023-08-19 11:40:45 -04:00
parent 5870b4f373
commit 50ac5db8d2
9 changed files with 192 additions and 27 deletions

View File

@ -53,6 +53,7 @@ set( OCC_LIBS
TKOffset TKOffset
TKOpenGl TKOpenGl
TKPrim TKPrim
TKRWMesh
TKService TKService
TKShHealing TKShHealing
TKSTEP209 TKSTEP209

View File

@ -50,7 +50,8 @@ public:
enum class FORMAT enum class FORMAT
{ {
UNKNOWN, // defefer to arg UNKNOWN, // defefer to arg
STEP STEP,
GLB
}; };
bool m_overwrite; bool m_overwrite;

View File

@ -54,7 +54,7 @@ CLI::PCB_EXPORT_3D_COMMAND::PCB_EXPORT_3D_COMMAND( const std::string& aName,
{ {
m_argParser.add_argument( ARG_FORMAT ) m_argParser.add_argument( ARG_FORMAT )
.default_value( std::string( "step" ) ) .default_value( std::string( "step" ) )
.help( UTF8STDSTR( _( "Output file format, options: step" ) ) ); .help( UTF8STDSTR( _( "Output file format, options: step, glb (binary glTF)" ) ) );
} }
m_argParser.add_argument( ARG_DRILL_ORIGIN ) m_argParser.add_argument( ARG_DRILL_ORIGIN )
@ -133,9 +133,9 @@ int CLI::PCB_EXPORT_3D_COMMAND::doPerform( KIWAY& aKiway )
wxString format = FROM_UTF8( m_argParser.get<std::string>( ARG_FORMAT ).c_str() ); wxString format = FROM_UTF8( m_argParser.get<std::string>( ARG_FORMAT ).c_str() );
if( format == wxS( "step" ) ) if( format == wxS( "step" ) )
{
step->m_format = JOB_EXPORT_PCB_3D::FORMAT::STEP; step->m_format = JOB_EXPORT_PCB_3D::FORMAT::STEP;
} else if( format == wxS( "glb" ) )
step->m_format = JOB_EXPORT_PCB_3D::FORMAT::GLB;
else else
{ {
wxFprintf( stderr, _( "Invalid format specified\n" ) ); wxFprintf( stderr, _( "Invalid format specified\n" ) );

View File

@ -184,6 +184,7 @@ static std::vector<COMMAND_ENTRY> commandStack = {
{ {
&exportPcbCmd, &exportPcbCmd,
{ {
&exportPcb3dCmd,
&exportPcbDrillCmd, &exportPcbDrillCmd,
&exportPcbDxfCmd, &exportPcbDxfCmd,
&exportPcbGerberCmd, &exportPcbGerberCmd,

View File

@ -110,6 +110,28 @@ private:
}; };
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 ) : EXPORTER_STEP::EXPORTER_STEP( BOARD* aBoard, const EXPORTER_STEP_PARAMS& aParams ) :
m_params( aParams ), m_params( aParams ),
m_error( false ), m_error( false ),
@ -458,9 +480,18 @@ bool EXPORTER_STEP::Export()
msg.Printf( _( "Board Thickness from stackup: %.3f mm\n" ), m_boardThickness ); msg.Printf( _( "Board Thickness from stackup: %.3f mm\n" ), m_boardThickness );
ReportMessage( msg ); 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 try
{ {
ReportMessage( _( "Build STEP data\n" ) ); ReportMessage( wxString::Format( _( "Build %s data\n" ), m_params.GetFormatName() ) );
if( !buildBoard3DShapes() ) if( !buildBoard3DShapes() )
{ {
@ -468,27 +499,37 @@ bool EXPORTER_STEP::Export()
return false; return false;
} }
ReportMessage( _( "Writing STEP file\n" ) ); ReportMessage( wxString::Format( _( "Writing %s file\n" ), m_params.GetFormatName() ) );
if( !m_pcbModel->WriteSTEP( m_outputFile ) ) bool success = true;
if( m_params.m_format == EXPORTER_STEP_PARAMS::FORMAT::STEP )
success = m_pcbModel->WriteSTEP( m_outputFile );
else if( m_params.m_format == EXPORTER_STEP_PARAMS::FORMAT::GLB )
success = m_pcbModel->WriteGLTF( m_outputFile );
if( !success )
{ {
ReportMessage( _( "\n** Error writing STEP file. **\n" ) ); ReportMessage( wxString::Format( _( "\n** Error writing %s file. **\n" ),
m_params.GetFormatName() ) );
return false; return false;
} }
else else
{ {
ReportMessage( wxString::Format( _( "\nSTEP file '%s' created.\n" ), m_outputFile ) ); ReportMessage( wxString::Format( _( "\%s file '%s' created.\n" ),
m_params.GetFormatName(), m_outputFile ) );
} }
} }
catch( const Standard_Failure& e ) catch( const Standard_Failure& e )
{ {
ReportMessage( e.GetMessageString() ); ReportMessage( e.GetMessageString() );
ReportMessage( _( "\n** Error exporting STEP file. Export aborted. **\n" ) ); ReportMessage( wxString::Format( _( "\n** Error exporting %s file. Export aborted. **\n" ),
m_params.GetFormatName() ) );
return false; return false;
} }
catch( ... ) catch( ... )
{ {
ReportMessage( _( "\n** Error exporting STEP file. Export aborted. **\n" ) ); ReportMessage( wxString::Format( _( "\n** Error exporting %s file. Export aborted. **\n" ),
m_params.GetFormatName() ) );
return false; return false;
} }
@ -496,12 +537,14 @@ bool EXPORTER_STEP::Export()
{ {
if( m_fail ) if( m_fail )
{ {
msg = _( "Unable to create STEP file.\n" msg = wxString::Format( _( "Unable to create %s file.\n"
"Check that the board has a valid outline and models." ); "Check that the board has a valid outline and models." ),
m_params.GetFormatName() );
} }
else if( m_error || m_warn ) else if( m_error || m_warn )
{ {
msg = _( "STEP file has been created, but there are warnings." ); msg = wxString::Format( _( "%s file has been created, but there are warnings." ),
m_params.GetFormatName() );
} }
ReportMessage( msg ); ReportMessage( msg );

View File

@ -54,9 +54,16 @@ public:
m_substModels( true ), m_substModels( true ),
m_BoardOutlinesChainingEpsilon( BOARD_DEFAULT_CHAINING_EPSILON ), m_BoardOutlinesChainingEpsilon( BOARD_DEFAULT_CHAINING_EPSILON ),
m_boardOnly( false ), m_boardOnly( false ),
m_exportTracks( false ) m_exportTracks( false ),
m_format( FORMAT::STEP )
{}; {};
enum class FORMAT
{
STEP,
GLB
};
wxString m_outputFile; wxString m_outputFile;
VECTOR2D m_origin; VECTOR2D m_origin;
@ -70,6 +77,10 @@ public:
double m_BoardOutlinesChainingEpsilon; double m_BoardOutlinesChainingEpsilon;
bool m_boardOnly; bool m_boardOnly;
bool m_exportTracks; bool m_exportTracks;
FORMAT m_format;
wxString GetDefaultExportExtension();
wxString GetFormatName();
}; };
class EXPORTER_STEP class EXPORTER_STEP

View File

@ -39,6 +39,8 @@
#include <footprint.h> #include <footprint.h>
#include <pad.h> #include <pad.h>
#include <kiplatform/io.h> #include <kiplatform/io.h>
#include <string_utils.h>
#include <build_version.h>
#include "step_pcb_model.h" #include "step_pcb_model.h"
#include "streamwrapper.h" #include "streamwrapper.h"
@ -89,6 +91,8 @@
#include <gp_Pnt.hxx> #include <gp_Pnt.hxx>
#include <Geom_BezierCurve.hxx> #include <Geom_BezierCurve.hxx>
#include <RWGltf_CafWriter.hxx>
#include <macros.h> #include <macros.h>
static constexpr double USER_PREC = 1e-4; static constexpr double USER_PREC = 1e-4;
@ -1038,7 +1042,7 @@ bool STEP_PCB_MODEL::getModelLabel( const std::string& aFileNameUTF8, VECTOR3D a
else // Substitution is not allowed else // Substitution is not allowed
{ {
if( aErrorMessage ) if( aErrorMessage )
aErrorMessage->Printf( wxT( "Cannot add a VRML model to a STEP file.\n" ) ); aErrorMessage->Printf( wxT( "Cannot load any VRML model for this export.\n" ) );
return false; return false;
} }
@ -1330,3 +1334,88 @@ TDF_Label STEP_PCB_MODEL::transferModel( Handle( TDocStd_Document )& source,
return component; return component;
} }
bool STEP_PCB_MODEL::WriteGLTF( const wxString& aFileName )
{
if( !isBoardOutlineValid() )
{
ReportMessage( wxString::Format( wxT( "No valid PCB assembly; cannot create output file "
"'%s'.\n" ),
aFileName ) );
return false;
}
TDF_LabelSequence freeShapes;
m_assy->GetFreeShapes( freeShapes );
ReportMessage( wxT( "Meshing model\n" ) );
// GLTF is a mesh format, we have to trigger opencascade to mesh the shapes we composited into the asesmbly
// To mesh models, lets just grab the free shape root and execute on them
for( Standard_Integer i = 1; i <= freeShapes.Length(); ++i )
{
TDF_Label label = freeShapes.Value( i );
TopoDS_Shape shape;
m_assy->GetShape( label, shape );
// These deflection values basically affect the accuracy of the mesh generated, a tighter
// deflection will result in larger meshes
// We could make this a tunable parameter, but for now fix it
const Standard_Real linearDeflection = 0.01;
const Standard_Real angularDeflection = 0.5;
BRepMesh_IncrementalMesh mesh( shape, linearDeflection, Standard_False, angularDeflection,
Standard_True );
}
wxFileName fn( aFileName );
const char* tmpGltfname = "$tempfile$.glb";
RWGltf_CafWriter cafWriter( tmpGltfname, true );
cafWriter.SetTransformationFormat( RWGltf_WriterTrsfFormat_Compact );
cafWriter.ChangeCoordinateSystemConverter().SetInputLengthUnit( 0.001 );
cafWriter.ChangeCoordinateSystemConverter().SetInputCoordinateSystem(
RWMesh_CoordinateSystem_Zup );
#if OCC_VERSION_HEX >= 0x070700
cafWriter.SetParallel( true );
#endif
TColStd_IndexedDataMapOfStringString metadata;
metadata.Add( TCollection_AsciiString( "pcb_name" ),
TCollection_ExtendedString( fn.GetName().wc_str() ) );
metadata.Add( TCollection_AsciiString( "source_pcb_file" ),
TCollection_ExtendedString( fn.GetFullName().wc_str() ) );
metadata.Add( TCollection_AsciiString( "generator" ),
TCollection_AsciiString( wxString::Format( wxS( "KiCad %s" ), GetSemanticVersion() ).ToAscii() ) );
metadata.Add( TCollection_AsciiString( "generated_at" ),
TCollection_AsciiString( GetISO8601CurrentDateTime().ToAscii() ) );
bool success = true;
// Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
wxString currCWD = wxGetCwd();
wxString workCWD = fn.GetPath();
if( !workCWD.IsEmpty() )
wxSetWorkingDirectory( workCWD );
success = cafWriter.Perform( m_doc, metadata, Message_ProgressRange() );
if( success )
{
// Preserve the permissions of the current file
KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpGltfname );
if( !wxRenameFile( tmpGltfname, fn.GetFullName(), true ) )
{
ReportMessage( wxString::Format( wxT( "Cannot rename temporary file '%s' to '%s'.\n" ),
tmpGltfname, fn.GetFullName() ) );
success = false;
}
}
wxSetWorkingDirectory( currCWD );
return success;
}

View File

@ -31,6 +31,7 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
#include <Standard_Version.hxx>
#include <BRepBuilderAPI_MakeWire.hxx> #include <BRepBuilderAPI_MakeWire.hxx>
#include <TDocStd_Document.hxx> #include <TDocStd_Document.hxx>
#include <XCAFApp_Application.hxx> #include <XCAFApp_Application.hxx>
@ -143,6 +144,22 @@ public:
// write the assembly model in STEP format // write the assembly model in STEP format
bool WriteSTEP( const wxString& aFileName ); bool WriteSTEP( const wxString& aFileName );
/**
* Write the assembly in binary GLTF Format
*
* We only support binary GLTF because GLTF is weird
* Officially, binary GLTF is actually json+binary in one file
* If we elected non-binary output with opecascade, it will generate
* that one file as two separate files, one containing json that references the binary
* Which is actually more annoying to deal with (to do the temp file rename, since we dont
* control the binary name) and silly when you can just have the one file.
*
* @param aFileName Output file path
*
* @return true if the write succeeded without error
*/
bool WriteGLTF( const wxString& aFileName );
private: private:
/** /**
* @return true if the board(s) outline is valid. False otherwise * @return true if the board(s) outline is valid. False otherwise

View File

@ -96,15 +96,6 @@ int PCBNEW_JOBS_HANDLER::JobExportStep( JOB* aJob )
BOARD* brd = LoadBoard( aStepJob->m_filename ); BOARD* brd = LoadBoard( aStepJob->m_filename );
if( aStepJob->m_outputFile.IsEmpty() )
{
wxFileName fn = brd->GetFileName();
fn.SetName( fn.GetName() );
fn.SetExt( wxS( "step" ) );
aStepJob->m_outputFile = fn.GetFullName();
}
EXPORTER_STEP_PARAMS params; EXPORTER_STEP_PARAMS params;
params.m_exportTracks = aStepJob->m_exportTracks; params.m_exportTracks = aStepJob->m_exportTracks;
params.m_includeUnspecified = aStepJob->m_includeUnspecified; params.m_includeUnspecified = aStepJob->m_includeUnspecified;
@ -117,6 +108,17 @@ int PCBNEW_JOBS_HANDLER::JobExportStep( JOB* aJob )
params.m_useGridOrigin = aStepJob->m_useGridOrigin; params.m_useGridOrigin = aStepJob->m_useGridOrigin;
params.m_boardOnly = aStepJob->m_boardOnly; params.m_boardOnly = aStepJob->m_boardOnly;
switch( aStepJob->m_format )
{
case JOB_EXPORT_PCB_3D::FORMAT::STEP:
params.m_format = EXPORTER_STEP_PARAMS::FORMAT::STEP;
break;
case JOB_EXPORT_PCB_3D::FORMAT::GLB:
params.m_format = EXPORTER_STEP_PARAMS::FORMAT::GLB;
break;
default: return CLI::EXIT_CODES::ERR_UNKNOWN; // should have gotten here
}
EXPORTER_STEP stepExporter( brd, params ); EXPORTER_STEP stepExporter( brd, params );
stepExporter.m_outputFile = aStepJob->m_outputFile; stepExporter.m_outputFile = aStepJob->m_outputFile;