/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2022 Mark Roszko * Copyright (C) 1992-2024 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 3 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, see . */ #include "command_pcb_export_3d.h" #include #include #include #include #include #include #include #include #define ARG_DRILL_ORIGIN "--drill-origin" #define ARG_GRID_ORIGIN "--grid-origin" #define ARG_NO_UNSPECIFIED "--no-unspecified" #define ARG_NO_DNP "--no-dnp" #define ARG_SUBST_MODELS "--subst-models" #define ARG_FORCE "--force" #define ARG_MIN_DISTANCE "--min-distance" #define ARG_USER_ORIGIN "--user-origin" #define ARG_BOARD_ONLY "--board-only" #define ARG_NO_BOARD_BODY "--no-board-body" #define ARG_NO_COMPONENTS "--no-components" #define ARG_INCLUDE_TRACKS "--include-tracks" #define ARG_INCLUDE_PADS "--include-pads" #define ARG_INCLUDE_ZONES "--include-zones" #define ARG_INCLUDE_INNER_COPPER "--include-inner-copper" #define ARG_INCLUDE_SILKSCREEN "--include-silkscreen" #define ARG_INCLUDE_SOLDERMASK "--include-soldermask" #define ARG_FUSE_SHAPES "--fuse-shapes" #define ARG_NO_OPTIMIZE_STEP "--no-optimize-step" #define ARG_NET_FILTER "--net-filter" #define ARG_FORMAT "--format" #define ARG_VRML_UNITS "--units" #define ARG_VRML_MODELS_DIR "--models-dir" #define ARG_VRML_MODELS_RELATIVE "--models-relative" #define ARG_COMPONENT_FILTER "--component-filter" #define REGEX_QUANTITY "([\\s]*[+-]?[\\d]*[.]?[\\d]*)" #define REGEX_DELIMITER "(?:[\\s]*x)" #define REGEX_UNIT "([m]{2}|(?:in))" CLI::PCB_EXPORT_3D_COMMAND::PCB_EXPORT_3D_COMMAND( const std::string& aName, const std::string& aDescription, JOB_EXPORT_PCB_3D::FORMAT aFormat ) : COMMAND( aName ), m_format( aFormat ) { m_argParser.add_description( aDescription ); addCommonArgs( true, true, false, false ); addDefineArg(); if( m_format == JOB_EXPORT_PCB_3D::FORMAT::UNKNOWN ) { m_argParser.add_argument( ARG_FORMAT ) .default_value( std::string( "step" ) ) .help( UTF8STDSTR( _( "Output file format, options: step, brep, xao, glb (binary glTF)" ) ) ); } m_argParser.add_argument( ARG_FORCE, "-f" ) .help( UTF8STDSTR( _( "Overwrite output file" ) ) ) .flag(); m_argParser.add_argument( ARG_NO_UNSPECIFIED ) .help( UTF8STDSTR( _( "Exclude 3D models for components with 'Unspecified' footprint type" ) ) ) .implicit_value( true ) .default_value( false ); m_argParser.add_argument( ARG_NO_DNP ) .help( UTF8STDSTR( _( "Exclude 3D models for components with 'Do not populate' attribute" ) ) ) .flag(); if( m_format == JOB_EXPORT_PCB_3D::FORMAT::STEP || m_format == JOB_EXPORT_PCB_3D::FORMAT::BREP || m_format == JOB_EXPORT_PCB_3D::FORMAT::XAO || m_format == JOB_EXPORT_PCB_3D::FORMAT::GLB ) { m_argParser.add_argument( ARG_GRID_ORIGIN ) .help( UTF8STDSTR( _( "Use Grid Origin for output origin" ) ) ) .flag(); m_argParser.add_argument( ARG_DRILL_ORIGIN ) .help( UTF8STDSTR( _( "Use Drill Origin for output origin" ) ) ) .flag(); m_argParser.add_argument( "--subst-models" ) .help( UTF8STDSTR( _( "Substitute STEP or IGS models with the same name in place " "of VRML models" ) ) ) .flag(); m_argParser.add_argument( ARG_BOARD_ONLY ) .help( UTF8STDSTR( _( "Only generate a board with no components" ) ) ) .flag(); m_argParser.add_argument( ARG_NO_BOARD_BODY ) .help( UTF8STDSTR( _( "Exclude board body" ) ) ) .flag(); m_argParser.add_argument( ARG_NO_COMPONENTS ) .help( UTF8STDSTR( _( "Exclude 3D models for components" ) ) ) .flag(); m_argParser.add_argument( ARG_COMPONENT_FILTER ) .help( UTF8STDSTR( _( "Only include component 3D models matching this list of " "reference designators (comma-separated, wildcards supported)" ) ) ) .default_value( std::string() ); m_argParser.add_argument( ARG_INCLUDE_TRACKS ) .help( UTF8STDSTR( _( "Export tracks and vias" ) ) ) .flag(); m_argParser.add_argument( ARG_INCLUDE_PADS ) .help( UTF8STDSTR( _( "Export pads" ) ) ) .flag(); m_argParser.add_argument( ARG_INCLUDE_ZONES ) .help( UTF8STDSTR( _( "Export zones" ) ) ) .flag(); m_argParser.add_argument( ARG_INCLUDE_INNER_COPPER ) .help( UTF8STDSTR( _( "Export elements on inner copper layers" ) ) ) .flag(); m_argParser.add_argument( ARG_INCLUDE_SILKSCREEN ) .help( UTF8STDSTR( _( "Export silkscreen graphics as a set of flat faces" ) ) ) .flag(); m_argParser.add_argument( ARG_INCLUDE_SOLDERMASK ) .help( UTF8STDSTR( _( "Export soldermask layers as a set of flat faces" ) ) ) .flag(); m_argParser.add_argument( ARG_FUSE_SHAPES ) .help( UTF8STDSTR( _( "Fuse overlapping geometry together" ) ) ) .flag(); m_argParser.add_argument( ARG_MIN_DISTANCE ) .default_value( std::string( "0.01mm" ) ) .help( UTF8STDSTR( _( "Minimum distance between points to treat them as separate ones" ) ) ) .metavar( "MIN_DIST" ); m_argParser.add_argument( ARG_NET_FILTER ) .default_value( std::string() ) .help( UTF8STDSTR( _( "Only include copper items belonging to nets matching this wildcard" ) ) ); } if( m_format == JOB_EXPORT_PCB_3D::FORMAT::STEP ) { m_argParser.add_argument( ARG_NO_OPTIMIZE_STEP ) .help( UTF8STDSTR( _( "Do not optimize STEP file (enables writing parametric " "curves)" ) ) ) .flag(); } m_argParser.add_argument( ARG_USER_ORIGIN ) .default_value( std::string() ) .help( UTF8STDSTR( _( "User-specified output origin ex. 1x1in, 1x1inch, 25.4x25.4mm " "(default unit mm)" ) ) ); if( m_format == JOB_EXPORT_PCB_3D::FORMAT::VRML ) { m_argParser.add_argument( ARG_VRML_UNITS ) .default_value( std::string( "in" ) ) .help( UTF8STDSTR( _( "Output units; valid options: mm, m, in, tenths" ) ) ); m_argParser.add_argument( ARG_VRML_MODELS_DIR ) .default_value( std::string( "" ) ) .help( UTF8STDSTR( _( "Name of folder to create and store 3d models in, if not specified or " "empty, the models will be embedded in main exported VRML file" ) ) ); m_argParser.add_argument( ARG_VRML_MODELS_RELATIVE ) .help( UTF8STDSTR( _( "Used with --models-dir to output relative paths in the " "resulting file" ) ) ) .flag(); } } int CLI::PCB_EXPORT_3D_COMMAND::doPerform( KIWAY& aKiway ) { std::unique_ptr step( new JOB_EXPORT_PCB_3D( true ) ); EXPORTER_STEP_PARAMS& params = step->m_3dparams; if( m_format == JOB_EXPORT_PCB_3D::FORMAT::STEP || m_format == JOB_EXPORT_PCB_3D::FORMAT::BREP || m_format == JOB_EXPORT_PCB_3D::FORMAT::XAO || m_format == JOB_EXPORT_PCB_3D::FORMAT::GLB ) { params.m_UseDrillOrigin = m_argParser.get( ARG_DRILL_ORIGIN ); params.m_UseGridOrigin = m_argParser.get( ARG_GRID_ORIGIN ); params.m_SubstModels = m_argParser.get( ARG_SUBST_MODELS ); params.m_ExportBoardBody = !m_argParser.get( ARG_NO_BOARD_BODY ); params.m_ExportComponents = !m_argParser.get( ARG_NO_COMPONENTS ); params.m_ExportTracksVias = m_argParser.get( ARG_INCLUDE_TRACKS ); params.m_ExportPads = m_argParser.get( ARG_INCLUDE_PADS ); params.m_ExportZones = m_argParser.get( ARG_INCLUDE_ZONES ); params.m_ExportInnerCopper = m_argParser.get( ARG_INCLUDE_INNER_COPPER ); params.m_ExportSilkscreen = m_argParser.get( ARG_INCLUDE_SILKSCREEN ); params.m_ExportSoldermask = m_argParser.get( ARG_INCLUDE_SOLDERMASK ); params.m_FuseShapes = m_argParser.get( ARG_FUSE_SHAPES ); params.m_BoardOnly = m_argParser.get( ARG_BOARD_ONLY ); params.m_NetFilter = From_UTF8( m_argParser.get( ARG_NET_FILTER ).c_str() ); params.m_ComponentFilter = From_UTF8( m_argParser.get( ARG_COMPONENT_FILTER ).c_str() ); } if( m_format == JOB_EXPORT_PCB_3D::FORMAT::STEP ) { params.m_OptimizeStep = !m_argParser.get( ARG_NO_OPTIMIZE_STEP ); } params.m_IncludeUnspecified = !m_argParser.get( ARG_NO_UNSPECIFIED ); params.m_IncludeDNP = !m_argParser.get( ARG_NO_DNP ); params.m_Overwrite = m_argParser.get( ARG_FORCE ); step->SetOutputPath( m_argOutput ); step->m_filename = m_argInput; step->m_format = m_format; step->SetVarOverrides( m_argDefineVars ); if( step->m_format == JOB_EXPORT_PCB_3D::FORMAT::UNKNOWN ) { 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( "brep" ) ) step->m_format = JOB_EXPORT_PCB_3D::FORMAT::BREP; else if( format == wxS( "xao" ) ) step->m_format = JOB_EXPORT_PCB_3D::FORMAT::XAO; else if( format == wxS( "glb" ) ) step->m_format = JOB_EXPORT_PCB_3D::FORMAT::GLB; else { wxFprintf( stderr, _( "Invalid format specified\n" ) ); return EXIT_CODES::ERR_ARGS; } } if( step->m_format == JOB_EXPORT_PCB_3D::FORMAT::VRML ) { wxString units = From_UTF8( m_argParser.get( ARG_VRML_UNITS ).c_str() ); if( units == wxS( "in" ) ) step->m_vrmlUnits = JOB_EXPORT_PCB_3D::VRML_UNITS::INCHES; else if( units == wxS( "mm" ) ) step->m_vrmlUnits = JOB_EXPORT_PCB_3D::VRML_UNITS::MILLIMETERS; else if( units == wxS( "m" ) ) step->m_vrmlUnits = JOB_EXPORT_PCB_3D::VRML_UNITS::METERS; else if( units == wxS( "tenths" ) ) step->m_vrmlUnits = JOB_EXPORT_PCB_3D::VRML_UNITS::TENTHS; else { wxFprintf( stderr, _( "Invalid units specified\n" ) ); return EXIT_CODES::ERR_ARGS; } step->m_vrmlModelDir = From_UTF8( m_argParser.get( ARG_VRML_MODELS_DIR ).c_str() ); step->m_vrmlRelativePaths = m_argParser.get( ARG_VRML_MODELS_RELATIVE ); } wxString userOrigin = From_UTF8( m_argParser.get( ARG_USER_ORIGIN ).c_str() ); LOCALE_IO dummy; // Switch to "C" locale if( !userOrigin.IsEmpty() ) { std::regex re_pattern( REGEX_QUANTITY REGEX_DELIMITER REGEX_QUANTITY REGEX_UNIT, std::regex_constants::icase ); std::smatch sm; std::string str( userOrigin.ToUTF8() ); std::regex_search( str, sm, re_pattern ); step->m_3dparams.m_Origin.x = atof( sm.str( 1 ).c_str() ); step->m_3dparams.m_Origin.y = atof( sm.str( 2 ).c_str() ); // Default unit for m_xOrigin and m_yOrigin is mm. // Convert in to board units. If the value is given in inches, it will be converted later step->m_3dparams.m_Origin.x = pcbIUScale.mmToIU( step->m_3dparams.m_Origin.x ); step->m_3dparams.m_Origin.y = pcbIUScale.mmToIU( step->m_3dparams.m_Origin.y ); std::string tunit( sm[3] ); if( tunit.size() > 0 ) // unit specified ( default = mm, but can be in, inch or mm ) { if( ( !sm.str( 1 ).compare( " " ) || !sm.str( 2 ).compare( " " ) ) || ( sm.size() != 4 ) ) { std::cout << m_argParser; return CLI::EXIT_CODES::ERR_ARGS; } // only in, inch and mm are valid: if( !tunit.compare( "in" ) || !tunit.compare( "inch" ) ) { step->m_3dparams.m_Origin *= 25.4; } else if( tunit.compare( "mm" ) ) { std::cout << m_argParser; return CLI::EXIT_CODES::ERR_ARGS; } } step->m_hasUserOrigin = true; } if( m_format == JOB_EXPORT_PCB_3D::FORMAT::STEP || m_format == JOB_EXPORT_PCB_3D::FORMAT::BREP || m_format == JOB_EXPORT_PCB_3D::FORMAT::XAO || m_format == JOB_EXPORT_PCB_3D::FORMAT::GLB ) { wxString minDistance = From_UTF8( m_argParser.get( ARG_MIN_DISTANCE ).c_str() ); if( !minDistance.IsEmpty() ) { std::regex re_pattern( REGEX_QUANTITY REGEX_UNIT, std::regex_constants::icase ); std::smatch sm; std::string str( minDistance.ToUTF8() ); std::regex_search( str, sm, re_pattern ); step->m_3dparams.m_BoardOutlinesChainingEpsilon = atof( sm.str( 1 ).c_str() ); std::string tunit( sm[2] ); if( tunit.size() > 0 ) // No unit accepted ( default = mm ) { if( !tunit.compare( "in" ) || !tunit.compare( "inch" ) ) { step->m_3dparams.m_BoardOutlinesChainingEpsilon *= 25.4; } else if( tunit.compare( "mm" ) ) { std::cout << m_argParser; return CLI::EXIT_CODES::ERR_ARGS; } } } } int exitCode = aKiway.ProcessJob( KIWAY::FACE_PCB, step.get() ); return exitCode; }