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
TKOpenGl
TKPrim
TKRWMesh
TKService
TKShHealing
TKSTEP209
@ -92,12 +93,12 @@ if(WIN32)
/opt/opencascade/lib
)
elseif(VCPKG_TOOLCHAIN)
FIND_PATH(OCC_INCLUDE_DIR
FIND_PATH(OCC_INCLUDE_DIR
NAMES Standard_Version.hxx
PATH_SUFFIXES
include/opencascade
)
FIND_LIBRARY(OCC_LIBRARY
FIND_LIBRARY(OCC_LIBRARY
NAMES TKernel
HINTS
${OCC_LIBRARY_DIR}

View File

@ -50,7 +50,8 @@ public:
enum class FORMAT
{
UNKNOWN, // defefer to arg
STEP
STEP,
GLB
};
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 )
.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 )
@ -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() );
if( format == wxS( "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
{
wxFprintf( stderr, _( "Invalid format specified\n" ) );

View File

@ -184,6 +184,7 @@ static std::vector<COMMAND_ENTRY> commandStack = {
{
&exportPcbCmd,
{
&exportPcb3dCmd,
&exportPcbDrillCmd,
&exportPcbDxfCmd,
&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 ) :
m_params( aParams ),
m_error( false ),
@ -458,9 +480,18 @@ bool EXPORTER_STEP::Export()
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( _( "Build STEP data\n" ) );
ReportMessage( wxString::Format( _( "Build %s data\n" ), m_params.GetFormatName() ) );
if( !buildBoard3DShapes() )
{
@ -468,27 +499,37 @@ bool EXPORTER_STEP::Export()
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;
}
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 )
{
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;
}
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;
}
@ -496,12 +537,14 @@ bool EXPORTER_STEP::Export()
{
if( m_fail )
{
msg = _( "Unable to create STEP file.\n"
"Check that the board has a valid outline and models." );
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 = _( "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 );

View File

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

View File

@ -39,6 +39,8 @@
#include <footprint.h>
#include <pad.h>
#include <kiplatform/io.h>
#include <string_utils.h>
#include <build_version.h>
#include "step_pcb_model.h"
#include "streamwrapper.h"
@ -89,6 +91,8 @@
#include <gp_Pnt.hxx>
#include <Geom_BezierCurve.hxx>
#include <RWGltf_CafWriter.hxx>
#include <macros.h>
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
{
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;
}
@ -1330,3 +1334,88 @@ TDF_Label STEP_PCB_MODEL::transferModel( Handle( TDocStd_Document )& source,
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 <vector>
#include <Standard_Version.hxx>
#include <BRepBuilderAPI_MakeWire.hxx>
#include <TDocStd_Document.hxx>
#include <XCAFApp_Application.hxx>
@ -143,6 +144,22 @@ public:
// write the assembly model in STEP format
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:
/**
* @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 );
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;
params.m_exportTracks = aStepJob->m_exportTracks;
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_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 );
stepExporter.m_outputFile = aStepJob->m_outputFile;