kicad-source/pcbnew/exporters/step/step_pcb_model.cpp
Marek Roszko edf6828cf1 Put the project name back into the step pcb label.
Software like Solidworks and other CAD treat STEP labels as unique entries.
When you import multi STEP files into the same project, it'll start deduplicating your design by those name.
So two completely unrelated PCBs with the same "PCB" name will result in one being replaced by another.
2023-03-20 21:11:28 -04:00

1272 lines
41 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 <algorithm>
#include <cmath>
#include <sstream>
#include <string>
#include <utility>
#include <wx/filename.h>
#include <wx/filefn.h>
#include <wx/stdpaths.h>
#include <wx/wfstream.h>
#include <wx/zipstrm.h>
#include <decompress.hpp>
#include <footprint.h>
#include <pad.h>
#include "step_pcb_model.h"
#include "streamwrapper.h"
#include <IGESCAFControl_Reader.hxx>
#include <IGESCAFControl_Writer.hxx>
#include <IGESControl_Controller.hxx>
#include <IGESData_GlobalSection.hxx>
#include <IGESData_IGESModel.hxx>
#include <Interface_Static.hxx>
#include <Quantity_Color.hxx>
#include <STEPCAFControl_Reader.hxx>
#include <STEPCAFControl_Writer.hxx>
#include <APIHeaderSection_MakeHeader.hxx>
#include <Standard_Version.hxx>
#include <TCollection_ExtendedString.hxx>
#include <TDataStd_Name.hxx>
#include <TDataStd_TreeNode.hxx>
#include <TDF_LabelSequence.hxx>
#include <TDF_ChildIterator.hxx>
#include <TopExp_Explorer.hxx>
#include <XCAFDoc.hxx>
#include <XCAFDoc_DocumentTool.hxx>
#include <XCAFDoc_ColorTool.hxx>
#include <BRep_Tool.hxx>
#include <BRepMesh_IncrementalMesh.hxx>
#include <BRepBuilderAPI.hxx>
#include <BRepBuilderAPI_MakeEdge.hxx>
#include <BRepBuilderAPI_Transform.hxx>
#include <BRepBuilderAPI_GTransform.hxx>
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepPrimAPI_MakePrism.hxx>
#include <BRepPrimAPI_MakeCylinder.hxx>
#include <BRepAlgoAPI_Cut.hxx>
#include <TopoDS.hxx>
#include <TopoDS_Wire.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Compound.hxx>
#include <TopoDS_Builder.hxx>
#include <Standard_Failure.hxx>
#include <gp_Ax2.hxx>
#include <gp_Circ.hxx>
#include <gp_Dir.hxx>
#include <gp_Pnt.hxx>
#include <Geom_BezierCurve.hxx>
#include <macros.h>
static constexpr double USER_PREC = 1e-4;
static constexpr double USER_ANGLE_PREC = 1e-6;
// nominal offset from the board
static constexpr double BOARD_OFFSET = 0.05;
// supported file types for 3D models
enum MODEL3D_FORMAT_TYPE
{
FMT_NONE,
FMT_STEP,
FMT_STEPZ,
FMT_IGES,
FMT_EMN,
FMT_IDF,
FMT_WRL,
FMT_WRZ
};
MODEL3D_FORMAT_TYPE fileType( const char* aFileName )
{
wxFileName lfile( wxString::FromUTF8Unchecked( aFileName ) );
if( !lfile.FileExists() )
{
wxString msg;
msg.Printf( wxT( " * fileType(): no such file: %s\n" ),
wxString::FromUTF8Unchecked( aFileName ) );
ReportMessage( msg );
return FMT_NONE;
}
wxString ext = lfile.GetExt().Lower();
if( ext == wxT( "wrl" ) )
return FMT_WRL;
if( ext == wxT( "wrz" ) )
return FMT_WRZ;
if( ext == wxT( "idf" ) )
return FMT_IDF; // component outline
if( ext == wxT( "emn" ) )
return FMT_EMN; // PCB assembly
if( ext == wxT( "stpz" ) || ext == wxT( "gz" ) )
return FMT_STEPZ;
OPEN_ISTREAM( ifile, aFileName );
if( ifile.fail() )
return FMT_NONE;
char iline[82];
memset( iline, 0, 82 );
ifile.getline( iline, 82 );
CLOSE_STREAM( ifile );
iline[81] = 0; // ensure NULL termination when string is too long
// check for STEP in Part 21 format
// (this can give false positives since Part 21 is not exclusively STEP)
if( !strncmp( iline, "ISO-10303-21;", 13 ) )
return FMT_STEP;
std::string fstr = iline;
// check for STEP in XML format
// (this can give both false positive and false negatives)
if( fstr.find( "urn:oid:1.0.10303." ) != std::string::npos )
return FMT_STEP;
// Note: this is a very simple test which can yield false positives; the only
// sure method for determining if a file *not* an IGES model is to attempt
// to load it.
if( iline[72] == 'S' && ( iline[80] == 0 || iline[80] == 13 || iline[80] == 10 ) )
return FMT_IGES;
return FMT_NONE;
}
STEP_PCB_MODEL::STEP_PCB_MODEL( const wxString& aPcbName )
{
m_app = XCAFApp_Application::GetApplication();
m_app->NewDocument( "MDTV-XCAF", m_doc );
m_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
m_assy_label = m_assy->NewShape();
m_hasPCB = false;
m_components = 0;
m_precision = USER_PREC;
m_angleprec = USER_ANGLE_PREC;
m_boardThickness = BOARD_THICKNESS_DEFAULT_MM;
m_copperThickness = COPPER_THICKNESS_DEFAULT_MM;
m_mergeOCCMaxDist = OCC_MAX_DISTANCE_TO_MERGE_POINTS;
m_minx = 1.0e10; // absurdly large number; any valid PCB X value will be smaller
m_pcbName = aPcbName;
BRepBuilderAPI::Precision( m_mergeOCCMaxDist );
m_maxError = pcbIUScale.mmToIU( ARC_TO_SEGMENT_MAX_ERROR_MM );
}
STEP_PCB_MODEL::~STEP_PCB_MODEL()
{
m_doc->Close();
}
bool STEP_PCB_MODEL::AddPadShape( const PAD* aPad, const VECTOR2D& aOrigin )
{
const std::shared_ptr<SHAPE_POLY_SET>& pad_shape = aPad->GetEffectivePolygon();
bool success = true;
VECTOR2I pos = aPad->GetPosition();
for( PCB_LAYER_ID pcb_layer = F_Cu; ; pcb_layer = B_Cu )
{
TopoDS_Shape curr_shape;
double Zpos = pcb_layer == F_Cu ? m_boardThickness : -m_copperThickness;
if( aPad->IsOnLayer( pcb_layer ) )
{
// Make a shape on top/bottom copper layer: a cylinder for rond shapes (pad or via)
// and a polygon for other shapes:
if( aPad->GetShape() == PAD_SHAPE::CIRCLE )
{
curr_shape = BRepPrimAPI_MakeCylinder(
pcbIUScale.IUTomm( aPad->GetSizeX() ) * 0.5, m_copperThickness ).Shape();
gp_Trsf shift;
shift.SetTranslation( gp_Vec( pcbIUScale.IUTomm( pos.x - aOrigin.x ),
-pcbIUScale.IUTomm( pos.y - aOrigin.y ),
Zpos ) );
BRepBuilderAPI_Transform round_shape( curr_shape, shift );
m_board_outlines.push_back( round_shape.Shape() );
}
else
{
success = MakeShape( curr_shape, pad_shape.get()->COutline(0), m_copperThickness, Zpos, aOrigin );
if( success )
m_board_outlines.push_back( curr_shape );
}
}
if( pcb_layer == B_Cu )
break;
}
if( !success ) // Error
ReportMessage( wxT( "OCC error adding pad/via polygon.\n" ) );
return success;
}
bool STEP_PCB_MODEL::AddCopperPolygonShapes( const SHAPE_POLY_SET* aPolyShapes, bool aOnTop, const VECTOR2D& aOrigin )
{
bool success = true;
for( int ii = 0; ii < aPolyShapes->OutlineCount(); ii++ )
{
TopoDS_Shape copper_shape;
double z_pos = aOnTop ? m_boardThickness : -m_copperThickness;
if( MakeShape( copper_shape, aPolyShapes->COutline( ii ), m_copperThickness, z_pos, aOrigin ) )
{
m_board_outlines.push_back( copper_shape );
}
else
{
ReportMessage( wxString::Format( wxT( "Could not add shape (%d points) to copper layer on %s.\n" ),
aPolyShapes->COutline( ii ).PointCount(),
aOnTop ? wxT( "top" ) : wxT( "bottom" ) ) );
success = false;
}
}
return success;
}
bool STEP_PCB_MODEL::AddPadHole( const PAD* aPad, const VECTOR2D& aOrigin )
{
if( aPad == nullptr || !aPad->GetDrillSize().x )
return false;
VECTOR2I pos = aPad->GetPosition();
const double margin = 0.01; // a small margin on the Z axix to be sure the hole
// is bigget than the board with copper
// must be > OCC_MAX_DISTANCE_TO_MERGE_POINTS
double holeZsize = m_boardThickness + ( m_copperThickness * 2 ) + ( margin * 2 );
if( aPad->GetDrillShape() == PAD_DRILL_SHAPE_CIRCLE )
{
TopoDS_Shape s =
BRepPrimAPI_MakeCylinder( pcbIUScale.IUTomm( aPad->GetDrillSize().x ) * 0.5, holeZsize ).Shape();
gp_Trsf shift;
shift.SetTranslation( gp_Vec( pcbIUScale.IUTomm( pos.x - aOrigin.x ),
-pcbIUScale.IUTomm( pos.y - aOrigin.y ),
-m_copperThickness - margin ) );
BRepBuilderAPI_Transform hole( s, shift );
m_cutouts.push_back( hole.Shape() );
return true;
}
// slotted hole
SHAPE_POLY_SET holeOutlines;
if( !aPad->TransformHoleToPolygon( holeOutlines, 0, m_maxError, ERROR_INSIDE ) )
{
return false;
}
TopoDS_Shape hole;
if( holeOutlines.OutlineCount() > 0 )
{
if( MakeShape( hole, holeOutlines.COutline( 0 ), holeZsize, -m_copperThickness - margin, aOrigin ) )
{
m_cutouts.push_back( hole );
}
}
else
{
return false;
}
return true;
}
bool STEP_PCB_MODEL::AddComponent( const std::string& aFileNameUTF8, const std::string& aRefDes,
bool aBottom, VECTOR2D aPosition, double aRotation, VECTOR3D aOffset,
VECTOR3D aOrientation, VECTOR3D aScale, bool aSubstituteModels )
{
if( aFileNameUTF8.empty() )
{
ReportMessage( wxString::Format( wxT( "No model defined for component %s.\n" ), aRefDes ) );
return false;
}
wxString fileName( wxString::FromUTF8( aFileNameUTF8.c_str() ) );
ReportMessage( wxString::Format( wxT( "Add component %s.\n" ), aRefDes ) );
// first retrieve a label
TDF_Label lmodel;
wxString errorMessage;
if( !getModelLabel( aFileNameUTF8, aScale, lmodel, aSubstituteModels, &errorMessage ) )
{
if( errorMessage.IsEmpty() )
ReportMessage( wxString::Format( wxT( "No model for filename '%s'.\n" ), fileName ) );
else
ReportMessage( errorMessage );
return false;
}
// calculate the Location transform
TopLoc_Location toploc;
if( !getModelLocation( aBottom, aPosition, aRotation, aOffset, aOrientation, toploc ) )
{
ReportMessage(
wxString::Format( wxT( "No location data for filename '%s'.\n" ), fileName ) );
return false;
}
// add the located sub-assembly
TDF_Label llabel = m_assy->AddComponent( m_assy_label, lmodel, toploc );
if( llabel.IsNull() )
{
ReportMessage( wxString::Format( wxT( "Could not add component with filename '%s'.\n" ),
fileName ) );
return false;
}
// attach the RefDes name
TCollection_ExtendedString refdes( aRefDes.c_str() );
TDataStd_Name::Set( llabel, refdes );
return true;
}
void STEP_PCB_MODEL::SetPCBThickness( double aThickness )
{
if( aThickness < 0.0 )
m_boardThickness = BOARD_THICKNESS_DEFAULT_MM;
else if( aThickness < BOARD_THICKNESS_MIN_MM )
m_boardThickness = BOARD_THICKNESS_MIN_MM;
else
m_boardThickness = aThickness;
}
void STEP_PCB_MODEL::SetBoardColor( double r, double g, double b )
{
m_boardColor[0] = r;
m_boardColor[1] = g;
m_boardColor[2] = b;
}
void STEP_PCB_MODEL::SetCopperColor( double r, double g, double b )
{
m_copperColor[0] = r;
m_copperColor[1] = g;
m_copperColor[2] = b;
}
void STEP_PCB_MODEL::OCCSetMergeMaxDistance( double aDistance )
{
// Ensure a minimal value (in mm)
m_mergeOCCMaxDist = aDistance;
BRepBuilderAPI::Precision( m_mergeOCCMaxDist );
}
bool STEP_PCB_MODEL::isBoardOutlineValid()
{
return m_pcb_labels.size() > 0;
}
bool STEP_PCB_MODEL::MakeShape( TopoDS_Shape& aShape, const SHAPE_LINE_CHAIN& aChain,
double aThickness, double aZposition, const VECTOR2D& aOrigin )
{
if( !aShape.IsNull() )
return false; // there is already data in the shape object
if( !aChain.IsClosed() )
return false; // the loop is not closed
BRepBuilderAPI_MakeWire wire;
bool success = true;
gp_Pnt start = gp_Pnt( pcbIUScale.IUTomm( aChain.CPoint( 0 ).x - aOrigin.x ),
-pcbIUScale.IUTomm( aChain.CPoint( 0 ).y - aOrigin.y ), aZposition );
int items_count = 0;
for( int j = 0; j < aChain.PointCount(); j++ )
{
int next = j+1;
gp_Pnt end;
if( next >= aChain.PointCount() )
next = 0;
end = gp_Pnt( pcbIUScale.IUTomm( aChain.CPoint( next ).x - aOrigin.x ),
-pcbIUScale.IUTomm( aChain.CPoint( next ).y - aOrigin.y ), aZposition );
// Do not export too short segments: they create broken shape because OCC thinks
// start point and end point are at the same place
double seg_len = std::hypot( end.X() - start.X(), end.Y() - start.Y() );
if( seg_len <= m_mergeOCCMaxDist )
continue;
try
{
TopoDS_Edge edge;
edge = BRepBuilderAPI_MakeEdge( start, end );
wire.Add( edge );
if( wire.Error() != BRepBuilderAPI_WireDone )
{
ReportMessage( wxT( "failed to add curve\n" ) );
return false;
}
items_count++;
}
catch( const Standard_Failure& e )
{
ReportMessage( wxString::Format( wxT( "MakeShape1: OCC exception: %s\n" ),
e.GetMessageString() ) );
success = false;
}
if( !success )
{
ReportMessage( wxS( "failed to add edge\n" ) );
return false;
}
start = end;
}
BRepBuilderAPI_MakeFace face;
try
{
face = BRepBuilderAPI_MakeFace( wire );
}
catch( const Standard_Failure& e )
{
ReportMessage(
wxString::Format( wxT( "MakeShape2 (items_count %d): OCC exception: %s\n" ),
items_count, e.GetMessageString() ) );
return false;
}
aShape = BRepPrimAPI_MakePrism( face, gp_Vec( 0, 0, aThickness ) );
if( aShape.IsNull() )
{
ReportMessage( wxT( "failed to create a prismatic shape\n" ) );
return false;
}
return true;
}
bool STEP_PCB_MODEL::CreatePCB( SHAPE_POLY_SET& aOutline, VECTOR2D aOrigin )
{
if( m_hasPCB )
{
if( !isBoardOutlineValid() )
return false;
return true;
}
m_hasPCB = true; // whether or not operations fail we note that CreatePCB has been invoked
// Number of items having the copper color
int copper_item_count = m_board_outlines.size();
// Support for more than one main outline (more than one board)
for( int cnt = 0; cnt < aOutline.OutlineCount(); cnt++ )
{
const SHAPE_LINE_CHAIN& outline = aOutline.COutline( cnt );
ReportMessage( wxString::Format( wxT( "Build board main outline %d with %d points.\n" ),
cnt+1, outline.PointCount() ) );
TopoDS_Shape curr_brd;
if( !MakeShape( curr_brd, outline, m_boardThickness, 0.0, aOrigin ) )
{
// Error
ReportMessage( wxString::Format(
wxT( "OCC error adding main outline polygon %d with %d points.\n" ),
cnt+1, outline.PointCount() ) );
}
else
m_board_outlines.push_back( curr_brd );
// Generate board cutouts in current main outline:
if( aOutline.HoleCount( cnt ) > 0 )
{
ReportMessage( wxString::Format( wxT( "Add cutouts in outline %d (%d cutout(s)).\n" ),
cnt+1, aOutline.HoleCount( cnt ) ) );
}
for( int ii = 0; ii < aOutline.HoleCount( cnt ); ii++ )
{
const SHAPE_LINE_CHAIN& holeOutline = aOutline.Hole( cnt, ii );
TopoDS_Shape hole;
if( MakeShape( hole, holeOutline, m_boardThickness + (m_copperThickness*4),
-m_copperThickness*2, aOrigin ) )
{
m_cutouts.push_back( hole );
}
}
}
// subtract cutouts (if any)
if( m_cutouts.size() )
{
ReportMessage( wxString::Format( wxT( "Build board cutouts and holes (%d hole(s)).\n" ),
(int) m_cutouts.size() ) );
TopTools_ListOfShape holelist;
for( TopoDS_Shape& hole : m_cutouts )
holelist.Append( hole );
// Remove holes for each item (board body or bodies, one can have more than one board)
// and copper items (copper_item_count items)
int cnt = 0;
for( TopoDS_Shape& board: m_board_outlines )
{
cnt++;
if( cnt % 10 == 0 )
ReportMessage( wxString::Format( wxT( "added %d/%d shapes\n" ),
cnt, (int)m_board_outlines.size() ) );
BRepAlgoAPI_Cut Cut;
TopTools_ListOfShape mainbrd;
mainbrd.Append( board );
Cut.SetArguments( mainbrd );
Cut.SetTools( holelist );
Cut.Build();
board = Cut.Shape();
}
}
// push the board to the data structure
ReportMessage( wxT( "\nGenerate board full shape.\n" ) );
// Dont expand the component or else coloring it gets hard
for( TopoDS_Shape& board: m_board_outlines )
{
m_pcb_labels.push_back( m_assy->AddComponent( m_assy_label, board, false ) );
if( m_pcb_labels.back().IsNull() )
return false;
}
// AddComponent adds a label that has a reference (not a parent/child relation) to the real
// label. We need to extract that real label to name it for the STEP output cleanly
// Why are we trying to name the bare board? Because CAD tools like SolidWorks do fun things
// like "deduplicate" imported STEPs by swapping STEP assembly components with already
// identically named assemblies. So we want to avoid having the PCB be generally defaulted
// to "Component" or "Assembly".
// Init colors for the board body and the copper items (if any)
Handle( XCAFDoc_ColorTool ) colorTool = XCAFDoc_DocumentTool::ColorTool( m_doc->Main() );
Quantity_Color board_color( m_boardColor[0], m_boardColor[1], m_boardColor[2],
Quantity_TOC_RGB );
Quantity_Color copper_color( m_copperColor[0], m_copperColor[1], m_copperColor[2],
Quantity_TOC_RGB );
int pcbIdx = 1;
int copper_objects_cnt = 0;
for( TDF_Label& pcb_label : m_pcb_labels )
{
Handle( TDataStd_TreeNode ) node;
if( pcb_label.FindAttribute( XCAFDoc::ShapeRefGUID(), node ) )
{
// Gives a name to each board object
TDF_Label label = node->Father()->Label();
if( !label.IsNull() )
{
wxString pcbName;
if( copper_objects_cnt < copper_item_count )
{
pcbName = wxString::Format( wxT( "Copper_Item%d" ), copper_objects_cnt+1 );
}
else
{
// Note, we include the pcb/project name as a prefix
// because several STEP importing CAD software like SolidWorks
// will deduplicate anything imported by it's STEP name
if( m_pcb_labels.size() == 1 )
pcbName = wxString::Format( wxT( "%s PCB" ), m_pcbName );
else
pcbName = wxString::Format( wxT( "%s PCB%d" ), m_pcbName, pcbIdx++ );
}
std::string pcbNameStdString( pcbName.ToUTF8() );
TCollection_ExtendedString partname( pcbNameStdString.c_str() );
TDataStd_Name::Set( label, partname );
}
}
// color the PCB
TopExp_Explorer topex;
topex.Init( m_assy->GetShape( pcb_label ), TopAbs_SOLID );
while( topex.More() )
{
// First objects are copper objects, last(s) is the board body
if( copper_objects_cnt < copper_item_count )
colorTool->SetColor( topex.Current(), copper_color, XCAFDoc_ColorSurf );
else
colorTool->SetColor( topex.Current(), board_color, XCAFDoc_ColorSurf );
topex.Next();
}
copper_objects_cnt++;
}
#if( defined OCC_VERSION_HEX ) && ( OCC_VERSION_HEX > 0x070101 )
m_assy->UpdateAssemblies();
#endif
return true;
}
#ifdef SUPPORTS_IGES
// write the assembly model in IGES format
bool STEP_PCB_MODEL::WriteIGES( const wxString& aFileName )
{
if( !isBoardOutlineValid() )
{
ReportMessage( wxString::Format( wxT( "No valid PCB assembly; cannot create output file "
"'%s'.\n" ),
aFileName ) );
return false;
}
wxFileName fn( aFileName );
IGESControl_Controller::Init();
IGESCAFControl_Writer writer;
writer.SetColorMode( Standard_True );
writer.SetNameMode( Standard_True );
IGESData_GlobalSection header = writer.Model()->GlobalSection();
header.SetFileName( new TCollection_HAsciiString( fn.GetFullName().ToAscii() ) );
header.SetSendName( new TCollection_HAsciiString( "KiCad electronic assembly" ) );
header.SetAuthorName(
new TCollection_HAsciiString( Interface_Static::CVal( "write.iges.header.author" ) ) );
header.SetCompanyName(
new TCollection_HAsciiString( Interface_Static::CVal( "write.iges.header.company" ) ) );
writer.Model()->SetGlobalSection( header );
if( Standard_False == writer.Perform( m_doc, aFileName.c_str() ) )
return false;
return true;
}
#endif
bool STEP_PCB_MODEL::WriteSTEP( const wxString& aFileName )
{
if( !isBoardOutlineValid() )
{
ReportMessage( wxString::Format( wxT( "No valid PCB assembly; cannot create output file "
"'%s'.\n" ),
aFileName ) );
return false;
}
wxFileName fn( aFileName );
STEPCAFControl_Writer writer;
writer.SetColorMode( Standard_True );
writer.SetNameMode( Standard_True );
// This must be set before we "transfer" the document.
// Should default to kicad_pcb.general.title_block.title,
// but in the meantime, defaulting to the basename of the output
// target is still better than "open cascade step translter v..."
// UTF8 should be ok from ISO 10303-21:2016, but... older stuff? use boring ascii
if( !Interface_Static::SetCVal( "write.step.product.name", fn.GetName().ToAscii() ) )
ReportMessage( wxT( "Failed to set step product name, but will attempt to continue." ) );
if( Standard_False == writer.Transfer( m_doc, STEPControl_AsIs ) )
return false;
APIHeaderSection_MakeHeader hdr( writer.ChangeWriter().Model() );
// Note: use only Ascii7 chars, non Ascii7 chars (therefore UFT8 chars)
// are creating issues in the step file
hdr.SetName( new TCollection_HAsciiString( fn.GetFullName().ToAscii() ) );
// TODO: how to control and ensure consistency with IGES?
hdr.SetAuthorValue( 1, new TCollection_HAsciiString( "Pcbnew" ) );
hdr.SetOrganizationValue( 1, new TCollection_HAsciiString( "Kicad" ) );
hdr.SetOriginatingSystem( new TCollection_HAsciiString( "KiCad to STEP converter" ) );
hdr.SetDescriptionValue( 1, new TCollection_HAsciiString( "KiCad electronic assembly" ) );
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 );
char tmpfname[] = "$tempfile$.step";
if( Standard_False == writer.Write( tmpfname ) )
success = false;
if( success )
{
if( !wxRenameFile( tmpfname, fn.GetFullName(), true ) )
{
ReportMessage( wxString::Format( wxT( "Cannot rename temporary file '%s' to '%s'.\n" ),
tmpfname,
fn.GetFullName() ) );
success = false;
}
}
wxSetWorkingDirectory( currCWD );
return success;
}
bool STEP_PCB_MODEL::getModelLabel( const std::string& aFileNameUTF8, VECTOR3D aScale, TDF_Label& aLabel,
bool aSubstituteModels, wxString* aErrorMessage )
{
std::string model_key = aFileNameUTF8 + "_" + std::to_string( aScale.x )
+ "_" + std::to_string( aScale.y ) + "_" + std::to_string( aScale.z );
MODEL_MAP::const_iterator mm = m_models.find( model_key );
if( mm != m_models.end() )
{
aLabel = mm->second;
return true;
}
aLabel.Nullify();
Handle( TDocStd_Document ) doc;
m_app->NewDocument( "MDTV-XCAF", doc );
wxString fileName( wxString::FromUTF8( aFileNameUTF8.c_str() ) );
MODEL3D_FORMAT_TYPE modelFmt = fileType( aFileNameUTF8.c_str() );
switch( modelFmt )
{
case FMT_IGES:
if( !readIGES( doc, aFileNameUTF8.c_str() ) )
{
ReportMessage( wxString::Format( wxT( "readIGES() failed on filename '%s'.\n" ),
fileName ) );
return false;
}
break;
case FMT_STEP:
if( !readSTEP( doc, aFileNameUTF8.c_str() ) )
{
ReportMessage( wxString::Format( wxT( "readSTEP() failed on filename '%s'.\n" ),
fileName ) );
return false;
}
break;
case FMT_STEPZ:
{
// To export a compressed step file (.stpz or .stp.gz file), the best way is to
// decaompress it in a temporaty file and load this temporary file
wxFFileInputStream ifile( fileName );
wxFileName outFile( fileName );
outFile.SetPath( wxStandardPaths::Get().GetTempDir() );
outFile.SetExt( wxT( "step" ) );
wxFileOffset size = ifile.GetLength();
if( size == wxInvalidOffset )
{
ReportMessage( wxString::Format( wxT( "getModelLabel() failed on filename '%s'.\n" ),
fileName ) );
return false;
}
{
bool success = false;
wxFFileOutputStream ofile( outFile.GetFullPath() );
if( !ofile.IsOk() )
return false;
char* buffer = new char[size];
ifile.Read( buffer, size );
std::string expanded;
try
{
expanded = gzip::decompress( buffer, size );
success = true;
}
catch( ... )
{
ReportMessage( wxString::Format( wxT( "failed to decompress '%s'.\n" ),
fileName ) );
}
if( expanded.empty() )
{
ifile.Reset();
ifile.SeekI( 0 );
wxZipInputStream izipfile( ifile );
std::unique_ptr<wxZipEntry> zip_file( izipfile.GetNextEntry() );
if( zip_file && !zip_file->IsDir() && izipfile.CanRead() )
{
izipfile.Read( ofile );
success = true;
}
}
else
{
ofile.Write( expanded.data(), expanded.size() );
}
delete[] buffer;
ofile.Close();
if( success )
{
std::string altFileNameUTF8 = TO_UTF8( outFile.GetFullPath() );
success =
getModelLabel( altFileNameUTF8, VECTOR3D( 1.0, 1.0, 1.0 ), aLabel, false );
}
return success;
}
break;
}
case FMT_WRL:
case FMT_WRZ:
/* WRL files are preferred for internal rendering, due to superior material properties, etc.
* However they are not suitable for MCAD export.
*
* If a .wrl file is specified, attempt to locate a replacement file for it.
*
* If a valid replacement file is found, the label for THAT file will be associated with
* the .wrl file
*/
if( aSubstituteModels )
{
wxFileName wrlName( fileName );
wxString basePath = wrlName.GetPath();
wxString baseName = wrlName.GetName();
// List of alternate files to look for
// Given in order of preference
// (Break if match is found)
wxArrayString alts;
// Step files
alts.Add( wxT( "stp" ) );
alts.Add( wxT( "step" ) );
alts.Add( wxT( "STP" ) );
alts.Add( wxT( "STEP" ) );
alts.Add( wxT( "Stp" ) );
alts.Add( wxT( "Step" ) );
alts.Add( wxT( "stpz" ) );
alts.Add( wxT( "stpZ" ) );
alts.Add( wxT( "STPZ" ) );
alts.Add( wxT( "step.gz" ) );
alts.Add( wxT( "stp.gz" ) );
// IGES files
alts.Add( wxT( "iges" ) );
alts.Add( wxT( "IGES" ) );
alts.Add( wxT( "igs" ) );
alts.Add( wxT( "IGS" ) );
//TODO - Other alternative formats?
for( const auto& alt : alts )
{
wxFileName altFile( basePath, baseName + wxT( "." ) + alt );
if( altFile.IsOk() && altFile.FileExists() )
{
std::string altFileNameUTF8 = TO_UTF8( altFile.GetFullPath() );
// When substituting a STEP/IGS file for VRML, do not apply the VRML scaling
// to the new STEP model. This process of auto-substitution is janky as all
// heck so let's not mix up un-displayed scale factors with potentially
// mis-matched files. And hope that the user doesn't have multiples files
// named "model.wrl" and "model.stp" referring to different parts.
// TODO: Fix model handling in v7. Default models should only be STP.
// Have option to override this in DISPLAY.
if( getModelLabel( altFileNameUTF8, VECTOR3D( 1.0, 1.0, 1.0 ), aLabel, false ) )
{
return true;
}
}
}
return false; // No replacement model found
}
else // Substitution is not allowed
{
if( aErrorMessage )
aErrorMessage->Printf( wxT( "Cannot add a VRML model to a STEP file.\n" ) );
return false;
}
break;
// TODO: implement IDF and EMN converters
default:
return false;
}
aLabel = transferModel( doc, m_doc, aScale );
if( aLabel.IsNull() )
{
ReportMessage( wxString::Format( wxT( "Could not transfer model data from file '%s'.\n" ),
fileName ) );
return false;
}
// attach the PART NAME ( base filename: note that in principle
// different models may have the same base filename )
wxFileName afile( fileName );
std::string pname( afile.GetName().ToUTF8() );
TCollection_ExtendedString partname( pname.c_str() );
TDataStd_Name::Set( aLabel, partname );
m_models.insert( MODEL_DATUM( model_key, aLabel ) );
++m_components;
return true;
}
bool STEP_PCB_MODEL::getModelLocation( bool aBottom, VECTOR2D aPosition, double aRotation, VECTOR3D aOffset, VECTOR3D aOrientation,
TopLoc_Location& aLocation )
{
// Order of operations:
// a. aOrientation is applied -Z*-Y*-X
// b. aOffset is applied
// Top ? add thickness to the Z offset
// c. Bottom ? Rotate on X axis (in contrast to most ECAD which mirror on Y),
// then rotate on +Z
// Top ? rotate on -Z
// d. aPosition is applied
//
// Note: Y axis is inverted in KiCad
gp_Trsf lPos;
lPos.SetTranslation( gp_Vec( aPosition.x, -aPosition.y, 0.0 ) );
// Offset board thickness
aOffset.z += BOARD_OFFSET;
gp_Trsf lRot;
if( aBottom )
{
lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), aRotation );
lPos.Multiply( lRot );
lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 1.0, 0.0, 0.0 ) ), M_PI );
lPos.Multiply( lRot );
}
else
{
aOffset.z += m_boardThickness;
lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), aRotation );
lPos.Multiply( lRot );
}
gp_Trsf lOff;
lOff.SetTranslation( gp_Vec( aOffset.x, aOffset.y, aOffset.z ) );
lPos.Multiply( lOff );
gp_Trsf lOrient;
lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ),
-aOrientation.z );
lPos.Multiply( lOrient );
lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 1.0, 0.0 ) ),
-aOrientation.y );
lPos.Multiply( lOrient );
lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 1.0, 0.0, 0.0 ) ),
-aOrientation.x );
lPos.Multiply( lOrient );
aLocation = TopLoc_Location( lPos );
return true;
}
bool STEP_PCB_MODEL::readIGES( Handle( TDocStd_Document )& doc, const char* fname )
{
IGESControl_Controller::Init();
IGESCAFControl_Reader reader;
IFSelect_ReturnStatus stat = reader.ReadFile( fname );
if( stat != IFSelect_RetDone )
return false;
// Enable user-defined shape precision
if( !Interface_Static::SetIVal( "read.precision.mode", 1 ) )
return false;
// Set the shape conversion precision to USER_PREC (default 0.0001 has too many triangles)
if( !Interface_Static::SetRVal( "read.precision.val", USER_PREC ) )
return false;
// set other translation options
reader.SetColorMode( true ); // use model colors
reader.SetNameMode( false ); // don't use IGES label names
reader.SetLayerMode( false ); // ignore LAYER data
if( !reader.Transfer( doc ) )
{
doc->Close();
return false;
}
// are there any shapes to translate?
if( reader.NbShapes() < 1 )
{
doc->Close();
return false;
}
return true;
}
bool STEP_PCB_MODEL::readSTEP( Handle( TDocStd_Document )& doc, const char* fname )
{
STEPCAFControl_Reader reader;
IFSelect_ReturnStatus stat = reader.ReadFile( fname );
if( stat != IFSelect_RetDone )
return false;
// Enable user-defined shape precision
if( !Interface_Static::SetIVal( "read.precision.mode", 1 ) )
return false;
// Set the shape conversion precision to USER_PREC (default 0.0001 has too many triangles)
if( !Interface_Static::SetRVal( "read.precision.val", USER_PREC ) )
return false;
// set other translation options
reader.SetColorMode( true ); // use model colors
reader.SetNameMode( false ); // don't use label names
reader.SetLayerMode( false ); // ignore LAYER data
if( !reader.Transfer( doc ) )
{
doc->Close();
return false;
}
// are there any shapes to translate?
if( reader.NbRootsForTransfer() < 1 )
{
doc->Close();
return false;
}
return true;
}
TDF_Label STEP_PCB_MODEL::transferModel( Handle( TDocStd_Document )& source,
Handle( TDocStd_Document )& dest, VECTOR3D aScale )
{
// transfer data from Source into a top level component of Dest
gp_GTrsf scale_transform;
scale_transform.SetVectorialPart( gp_Mat( aScale.x, 0, 0,
0, aScale.y, 0,
0, 0, aScale.z ) );
BRepBuilderAPI_GTransform brep( scale_transform );
// s_assy = shape tool for the source
Handle(XCAFDoc_ShapeTool) s_assy = XCAFDoc_DocumentTool::ShapeTool( source->Main() );
// retrieve all free shapes within the assembly
TDF_LabelSequence frshapes;
s_assy->GetFreeShapes( frshapes );
// d_assy = shape tool for the destination
Handle( XCAFDoc_ShapeTool ) d_assy = XCAFDoc_DocumentTool::ShapeTool ( dest->Main() );
// create a new shape within the destination and set the assembly tool to point to it
TDF_Label component = d_assy->NewShape();
int nshapes = frshapes.Length();
int id = 1;
Handle( XCAFDoc_ColorTool ) scolor = XCAFDoc_DocumentTool::ColorTool( source->Main() );
Handle( XCAFDoc_ColorTool ) dcolor = XCAFDoc_DocumentTool::ColorTool( dest->Main() );
TopExp_Explorer dtop;
TopExp_Explorer stop;
while( id <= nshapes )
{
TopoDS_Shape shape = s_assy->GetShape( frshapes.Value( id ) );
if( !shape.IsNull() )
{
TopoDS_Shape scaled_shape( shape );
if( aScale.x != 1.0 || aScale.y != 1.0 || aScale.z != 1.0 )
{
brep.Perform( shape, Standard_False );
if( brep.IsDone() )
{
scaled_shape = brep.Shape();
}
else
{
ReportMessage( wxT( " * transfertModel(): failed to scale model\n" ) );
scaled_shape = shape;
}
}
TDF_Label niulab = d_assy->AddComponent( component, scaled_shape, Standard_False );
// check for per-surface colors
stop.Init( shape, TopAbs_FACE );
dtop.Init( d_assy->GetShape( niulab ), TopAbs_FACE );
while( stop.More() && dtop.More() )
{
Quantity_Color face_color;
TDF_Label tl;
// give priority to the base shape's color
if( s_assy->FindShape( stop.Current(), tl ) )
{
if( scolor->GetColor( tl, XCAFDoc_ColorSurf, face_color )
|| scolor->GetColor( tl, XCAFDoc_ColorGen, face_color )
|| scolor->GetColor( tl, XCAFDoc_ColorCurv, face_color ) )
{
dcolor->SetColor( dtop.Current(), face_color, XCAFDoc_ColorSurf );
}
}
else if( scolor->GetColor( stop.Current(), XCAFDoc_ColorSurf, face_color )
|| scolor->GetColor( stop.Current(), XCAFDoc_ColorGen, face_color )
|| scolor->GetColor( stop.Current(), XCAFDoc_ColorCurv, face_color ) )
{
dcolor->SetColor( dtop.Current(), face_color, XCAFDoc_ColorSurf );
}
stop.Next();
dtop.Next();
}
// check for per-solid colors
stop.Init( shape, TopAbs_SOLID );
dtop.Init( d_assy->GetShape( niulab ), TopAbs_SOLID, TopAbs_FACE );
while( stop.More() && dtop.More() )
{
Quantity_Color face_color;
TDF_Label tl;
// give priority to the base shape's color
if( s_assy->FindShape( stop.Current(), tl ) )
{
if( scolor->GetColor( tl, XCAFDoc_ColorSurf, face_color )
|| scolor->GetColor( tl, XCAFDoc_ColorGen, face_color )
|| scolor->GetColor( tl, XCAFDoc_ColorCurv, face_color ) )
{
dcolor->SetColor( dtop.Current(), face_color, XCAFDoc_ColorGen );
}
}
else if( scolor->GetColor( stop.Current(), XCAFDoc_ColorSurf, face_color )
|| scolor->GetColor( stop.Current(), XCAFDoc_ColorGen, face_color )
|| scolor->GetColor( stop.Current(), XCAFDoc_ColorCurv, face_color ) )
{
dcolor->SetColor( dtop.Current(), face_color, XCAFDoc_ColorSurf );
}
stop.Next();
dtop.Next();
}
}
++id;
};
return component;
}