diff --git a/cmake/FindOCC.cmake b/cmake/FindOCC.cmake index af249c9ce2..306bca6952 100644 --- a/cmake/FindOCC.cmake +++ b/cmake/FindOCC.cmake @@ -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} diff --git a/common/jobs/job_export_pcb_3d.h b/common/jobs/job_export_pcb_3d.h index 93fc75df40..9bbb93b9dd 100644 --- a/common/jobs/job_export_pcb_3d.h +++ b/common/jobs/job_export_pcb_3d.h @@ -50,7 +50,8 @@ public: enum class FORMAT { UNKNOWN, // defefer to arg - STEP + STEP, + GLB }; bool m_overwrite; diff --git a/kicad/cli/command_pcb_export_3d.cpp b/kicad/cli/command_pcb_export_3d.cpp index e111b8320c..67773c0bd3 100644 --- a/kicad/cli/command_pcb_export_3d.cpp +++ b/kicad/cli/command_pcb_export_3d.cpp @@ -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( 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" ) ); diff --git a/kicad/kicad_cli.cpp b/kicad/kicad_cli.cpp index 41c4a2d0c2..3d1ead8d6d 100644 --- a/kicad/kicad_cli.cpp +++ b/kicad/kicad_cli.cpp @@ -184,6 +184,7 @@ static std::vector commandStack = { { &exportPcbCmd, { + &exportPcb3dCmd, &exportPcbDrillCmd, &exportPcbDxfCmd, &exportPcbGerberCmd, diff --git a/pcbnew/exporters/step/exporter_step.cpp b/pcbnew/exporters/step/exporter_step.cpp index 25bcf60fdd..73736a6ab8 100644 --- a/pcbnew/exporters/step/exporter_step.cpp +++ b/pcbnew/exporters/step/exporter_step.cpp @@ -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 ); diff --git a/pcbnew/exporters/step/exporter_step.h b/pcbnew/exporters/step/exporter_step.h index 02c4143816..2963adfc95 100644 --- a/pcbnew/exporters/step/exporter_step.h +++ b/pcbnew/exporters/step/exporter_step.h @@ -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 diff --git a/pcbnew/exporters/step/step_pcb_model.cpp b/pcbnew/exporters/step/step_pcb_model.cpp index ff4eb5936a..9c7b3bc4a2 100644 --- a/pcbnew/exporters/step/step_pcb_model.cpp +++ b/pcbnew/exporters/step/step_pcb_model.cpp @@ -39,6 +39,8 @@ #include #include #include +#include +#include #include "step_pcb_model.h" #include "streamwrapper.h" @@ -89,6 +91,8 @@ #include #include +#include + #include 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; +} \ No newline at end of file diff --git a/pcbnew/exporters/step/step_pcb_model.h b/pcbnew/exporters/step/step_pcb_model.h index 71c809d2b7..26931d4e5f 100644 --- a/pcbnew/exporters/step/step_pcb_model.h +++ b/pcbnew/exporters/step/step_pcb_model.h @@ -31,6 +31,7 @@ #include #include +#include #include #include #include @@ -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 diff --git a/pcbnew/pcbnew_jobs_handler.cpp b/pcbnew/pcbnew_jobs_handler.cpp index bbbdf317cd..c4969e7735 100644 --- a/pcbnew/pcbnew_jobs_handler.cpp +++ b/pcbnew/pcbnew_jobs_handler.cpp @@ -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;