You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2981 lines
95 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
1 year ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
5 years ago
4 years ago
5 years ago
5 years ago
4 years ago
4 years ago
5 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2022 Mark Roszko <mark.roszko@gmail.com>
  5. * Copyright (C) 2016 Cirilo Bernardo <cirilo.bernardo@gmail.com>
  6. * Copyright (C) 2016-2024 KiCad Developers, see AUTHORS.txt for contributors.
  7. *
  8. * This program is free software; you can redistribute it and/or
  9. * modify it under the terms of the GNU General Public License
  10. * as published by the Free Software Foundation; either version 2
  11. * of the License, or (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License
  19. * along with this program; if not, you may find one here:
  20. * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  21. * or you may search the http://www.gnu.org website for the version 2 license,
  22. * or you may write to the Free Software Foundation, Inc.,
  23. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
  24. */
  25. #include <algorithm>
  26. #include <cmath>
  27. #include <sstream>
  28. #include <string>
  29. #include <utility>
  30. #include <wx/filename.h>
  31. #include <wx/filefn.h>
  32. #include <wx/stdpaths.h>
  33. #include <wx/wfstream.h>
  34. #include <wx/zipstrm.h>
  35. #include <wx/stdstream.h>
  36. #include <decompress.hpp>
  37. #include <footprint.h>
  38. #include <pad.h>
  39. #include <pcb_track.h>
  40. #include <kiplatform/io.h>
  41. #include <string_utils.h>
  42. #include <build_version.h>
  43. #include <geometry/shape_segment.h>
  44. #include <geometry/shape_circle.h>
  45. #include <board_stackup_manager/board_stackup.h>
  46. #include <board_stackup_manager/stackup_predefined_prms.h>
  47. #include "step_pcb_model.h"
  48. #include "streamwrapper.h"
  49. #include <IGESCAFControl_Reader.hxx>
  50. #include <IGESCAFControl_Writer.hxx>
  51. #include <IGESControl_Controller.hxx>
  52. #include <IGESData_GlobalSection.hxx>
  53. #include <IGESData_IGESModel.hxx>
  54. #include <Interface_Static.hxx>
  55. #include <Quantity_Color.hxx>
  56. #include <STEPCAFControl_Reader.hxx>
  57. #include <STEPCAFControl_Writer.hxx>
  58. #include <APIHeaderSection_MakeHeader.hxx>
  59. #include <Standard_Failure.hxx>
  60. #include <Standard_Handle.hxx>
  61. #include <Standard_Version.hxx>
  62. #include <TCollection_ExtendedString.hxx>
  63. #include <TDocStd_Document.hxx>
  64. #include <TDocStd_XLinkTool.hxx>
  65. #include <TDataStd_Name.hxx>
  66. #include <TDataStd_TreeNode.hxx>
  67. #include <TDF_LabelSequence.hxx>
  68. #include <TDF_Tool.hxx>
  69. #include <TopExp_Explorer.hxx>
  70. #include <TopoDS.hxx>
  71. #include <XCAFApp_Application.hxx>
  72. #include <XCAFDoc.hxx>
  73. #include <XCAFDoc_DocumentTool.hxx>
  74. #include <XCAFDoc_ColorTool.hxx>
  75. #include <XCAFDoc_ShapeTool.hxx>
  76. #include <XCAFDoc_VisMaterialTool.hxx>
  77. #include <XCAFDoc_Area.hxx>
  78. #include <XCAFDoc_Centroid.hxx>
  79. #include <XCAFDoc_Location.hxx>
  80. #include <XCAFDoc_Volume.hxx>
  81. #include "KI_XCAFDoc_AssemblyGraph.hxx"
  82. #include <BRep_Tool.hxx>
  83. #include <BRepMesh_IncrementalMesh.hxx>
  84. #include <BRepBuilderAPI_GTransform.hxx>
  85. #include <BRepBuilderAPI_MakeEdge.hxx>
  86. #include <BRepBuilderAPI_MakeWire.hxx>
  87. #include <BRepBuilderAPI_MakeFace.hxx>
  88. #include <BRepExtrema_DistShapeShape.hxx>
  89. #include <BRepPrimAPI_MakePrism.hxx>
  90. #include <BRepTools.hxx>
  91. #include <BRepLib_MakeWire.hxx>
  92. #include <BRepAdaptor_Surface.hxx>
  93. #include <BRepAlgoAPI_Check.hxx>
  94. #include <BRepAlgoAPI_Cut.hxx>
  95. #include <BRepAlgoAPI_Fuse.hxx>
  96. #include <ShapeUpgrade_UnifySameDomain.hxx>
  97. #include <BRepBndLib.hxx>
  98. #include <Bnd_BoundSortBox.hxx>
  99. #include <Geom_Curve.hxx>
  100. #include <Geom_TrimmedCurve.hxx>
  101. #include <gp_Ax2.hxx>
  102. #include <gp_Dir.hxx>
  103. #include <gp_Pnt.hxx>
  104. #include <GC_MakeArcOfCircle.hxx>
  105. #include <GC_MakeCircle.hxx>
  106. #include <RWGltf_CafWriter.hxx>
  107. #if OCC_VERSION_HEX >= 0x070700
  108. #include <VrmlAPI_CafReader.hxx>
  109. #endif
  110. #include <macros.h>
  111. static constexpr double USER_PREC = 1e-4;
  112. static constexpr double USER_ANGLE_PREC = 1e-6;
  113. // nominal offset from the board
  114. static constexpr double BOARD_OFFSET = 0.05;
  115. // supported file types for 3D models
  116. enum MODEL3D_FORMAT_TYPE
  117. {
  118. FMT_NONE,
  119. FMT_STEP,
  120. FMT_STEPZ,
  121. FMT_IGES,
  122. FMT_EMN,
  123. FMT_IDF,
  124. FMT_WRL,
  125. FMT_WRZ
  126. };
  127. MODEL3D_FORMAT_TYPE fileType( const char* aFileName )
  128. {
  129. wxFileName lfile( wxString::FromUTF8Unchecked( aFileName ) );
  130. if( !lfile.FileExists() )
  131. {
  132. wxString msg;
  133. msg.Printf( wxT( " * fileType(): no such file: %s\n" ),
  134. wxString::FromUTF8Unchecked( aFileName ) );
  135. ReportMessage( msg );
  136. return FMT_NONE;
  137. }
  138. wxString ext = lfile.GetExt().Lower();
  139. if( ext == wxT( "wrl" ) )
  140. return FMT_WRL;
  141. if( ext == wxT( "wrz" ) )
  142. return FMT_WRZ;
  143. if( ext == wxT( "idf" ) )
  144. return FMT_IDF; // component outline
  145. if( ext == wxT( "emn" ) )
  146. return FMT_EMN; // PCB assembly
  147. if( ext == wxT( "stpz" ) || ext == wxT( "gz" ) )
  148. return FMT_STEPZ;
  149. OPEN_ISTREAM( ifile, aFileName );
  150. if( ifile.fail() )
  151. return FMT_NONE;
  152. char iline[82];
  153. MODEL3D_FORMAT_TYPE format_type = FMT_NONE;
  154. // The expected header should be the first line.
  155. // However some files can have a comment at the beginning of the file
  156. // So read up to max_line_count lines to try to find the actual header
  157. const int max_line_count = 3;
  158. for( int ii = 0; ii < max_line_count; ii++ )
  159. {
  160. memset( iline, 0, 82 );
  161. ifile.getline( iline, 82 );
  162. iline[81] = 0; // ensure NULL termination when string is too long
  163. // check for STEP in Part 21 format
  164. // (this can give false positives since Part 21 is not exclusively STEP)
  165. if( !strncmp( iline, "ISO-10303-21;", 13 ) )
  166. {
  167. format_type = FMT_STEP;
  168. break;
  169. }
  170. std::string fstr = iline;
  171. // check for STEP in XML format
  172. // (this can give both false positive and false negatives)
  173. if( fstr.find( "urn:oid:1.0.10303." ) != std::string::npos )
  174. {
  175. format_type = FMT_STEP;
  176. break;
  177. }
  178. // Note: this is a very simple test which can yield false positives; the only
  179. // sure method for determining if a file *not* an IGES model is to attempt
  180. // to load it.
  181. if( iline[72] == 'S' && ( iline[80] == 0 || iline[80] == 13 || iline[80] == 10 ) )
  182. {
  183. format_type = FMT_IGES;
  184. break;
  185. }
  186. // Only a comment (starting by "/*") is allowed as header
  187. if( strncmp( iline, "/*", 2 ) != 0 ) // not a comment
  188. break;
  189. }
  190. CLOSE_STREAM( ifile );
  191. return format_type;
  192. }
  193. static VECTOR2D CircleCenterFrom3Points( const VECTOR2D& p1, const VECTOR2D& p2,
  194. const VECTOR2D& p3 )
  195. {
  196. VECTOR2D center;
  197. // Move coordinate origin to p2, to simplify calculations
  198. VECTOR2D b = p1 - p2;
  199. VECTOR2D d = p3 - p2;
  200. double bc = ( b.x * b.x + b.y * b.y ) / 2.0;
  201. double cd = ( -d.x * d.x - d.y * d.y ) / 2.0;
  202. double det = -b.x * d.y + d.x * b.y;
  203. // We're fine with divisions by 0
  204. det = 1.0 / det;
  205. center.x = ( -bc * d.y - cd * b.y ) * det;
  206. center.y = ( b.x * cd + d.x * bc ) * det;
  207. center += p2;
  208. return center;
  209. }
  210. #define APPROX_DBG( stmt )
  211. //#define APPROX_DBG( stmt ) stmt
  212. static SHAPE_LINE_CHAIN approximateLineChainWithArcs( const SHAPE_LINE_CHAIN& aSrc )
  213. {
  214. // An algo that takes 3 points, calculates a circle center,
  215. // then tries to find as many points fitting the circle.
  216. static const double c_radiusDeviation = 1000.0;
  217. static const double c_arcCenterDeviation = 1000.0;
  218. static const double c_relLengthDeviation = 0.8;
  219. static const int c_last_none = -1000; // Meaning the arc cannot be constructed
  220. // Allow larger angles for segments below this size
  221. static const double c_smallSize = pcbIUScale.mmToIU( 0.1 );
  222. static const double c_circleCloseGap = pcbIUScale.mmToIU( 1.0 );
  223. APPROX_DBG( std::cout << std::endl );
  224. if( aSrc.PointCount() < 4 )
  225. return aSrc;
  226. if( !aSrc.IsClosed() )
  227. return aSrc; // non-closed polygons are not supported
  228. SHAPE_LINE_CHAIN dst;
  229. int jEndIdx = aSrc.PointCount() - 3;
  230. for( int i = 0; i < aSrc.PointCount(); i++ )
  231. {
  232. int first = i - 3;
  233. int last = c_last_none;
  234. VECTOR2D p0 = aSrc.CPoint( i - 3 );
  235. VECTOR2D p1 = aSrc.CPoint( i - 2 );
  236. VECTOR2D p2 = aSrc.CPoint( i - 1 );
  237. APPROX_DBG( std::cout << i << " " << aSrc.CPoint( i ) << " " << ( i - 3 ) << " "
  238. << VECTOR2I( p0 ) << " " << ( i - 2 ) << " " << VECTOR2I( p1 ) << " "
  239. << ( i - 1 ) << " " << VECTOR2I( p2 ) << std::endl );
  240. VECTOR2D v01 = p1 - p0;
  241. VECTOR2D v12 = p2 - p1;
  242. bool defective = false;
  243. double d01 = v01.EuclideanNorm();
  244. double d12 = v12.EuclideanNorm();
  245. // Check distance differences between 3 first points
  246. defective |= std::abs( d01 - d12 ) > ( std::max( d01, d12 ) * c_relLengthDeviation );
  247. if( !defective )
  248. {
  249. // Check angles between 3 first points
  250. EDA_ANGLE a01( v01 );
  251. EDA_ANGLE a12( v12 );
  252. double a_diff = ( a01 - a12 ).Normalize180().AsDegrees();
  253. defective |= std::abs( a_diff ) < 0.1;
  254. // Larger angles are allowed for smaller geometry
  255. double maxAngleDiff = std::max( d01, d12 ) < c_smallSize ? 46.0 : 30.0;
  256. defective |= std::abs( a_diff ) >= maxAngleDiff;
  257. }
  258. if( !defective )
  259. {
  260. // Find last point lying on the circle created from 3 first points
  261. VECTOR2D center = CircleCenterFrom3Points( p0, p1, p2 );
  262. double radius = ( p0 - center ).EuclideanNorm();
  263. VECTOR2D p_prev = p2;
  264. EDA_ANGLE a_prev( v12 );
  265. for( int j = i; j <= jEndIdx; j++ )
  266. {
  267. VECTOR2D p_test = aSrc.CPoint( j );
  268. EDA_ANGLE a_test( p_test - p_prev );
  269. double rad_test = ( p_test - center ).EuclideanNorm();
  270. double d_tl = ( p_test - p_prev ).EuclideanNorm();
  271. double rad_dev = std::abs( radius - rad_test );
  272. APPROX_DBG( std::cout << " " << j << " " << aSrc.CPoint( j ) << " rad "
  273. << int64_t( rad_test ) << " ref " << int64_t( radius )
  274. << std::endl );
  275. if( rad_dev > c_radiusDeviation )
  276. {
  277. APPROX_DBG( std::cout << " " << j
  278. << " Radius deviation too large: " << int64_t( rad_dev )
  279. << " > " << c_radiusDeviation << std::endl );
  280. break;
  281. }
  282. // Larger angles are allowed for smaller geometry
  283. double maxAngleDiff =
  284. std::max( std::max( d01, d12 ), d_tl ) < c_smallSize ? 46.0 : 30.0;
  285. double a_diff_test = ( a_prev - a_test ).Normalize180().AsDegrees();
  286. if( std::abs( a_diff_test ) >= maxAngleDiff )
  287. {
  288. APPROX_DBG( std::cout << " " << j << " Angles differ too much " << a_diff_test
  289. << std::endl );
  290. break;
  291. }
  292. if( std::abs( d_tl - d01 ) > ( std::max( d_tl, d01 ) * c_relLengthDeviation ) )
  293. {
  294. APPROX_DBG( std::cout << " " << j << " Lengths differ too much " << d_tl
  295. << "; " << d01 << std::endl );
  296. break;
  297. }
  298. last = j;
  299. p_prev = p_test;
  300. a_prev = a_test;
  301. }
  302. }
  303. if( last != c_last_none )
  304. {
  305. // Try to add an arc, testing for self-interference
  306. SHAPE_ARC arc( aSrc.CPoint( first ), aSrc.CPoint( ( first + last ) / 2 ),
  307. aSrc.CPoint( last ), 0 );
  308. if( last > aSrc.PointCount() - 3 && !dst.IsArcSegment( 0 ) )
  309. {
  310. // If we've found an arc at the end, but already added segments at the start, remove them.
  311. int toRemove = last - ( aSrc.PointCount() - 3 );
  312. while( toRemove )
  313. {
  314. dst.RemoveShape( 0 );
  315. toRemove--;
  316. }
  317. }
  318. SHAPE_LINE_CHAIN testChain = dst;
  319. testChain.Append( arc );
  320. testChain.Append( aSrc.Slice( last, std::max( last, aSrc.PointCount() - 3 ) ) );
  321. testChain.SetClosed( aSrc.IsClosed() );
  322. if( !testChain.SelfIntersectingWithArcs() )
  323. {
  324. // Add arc
  325. dst.Append( arc );
  326. APPROX_DBG( std::cout << " Add arc start " << arc.GetP0() << " mid "
  327. << arc.GetArcMid() << " end " << arc.GetP1() << std::endl );
  328. i = last + 3;
  329. }
  330. else
  331. {
  332. // Self-interference
  333. last = c_last_none;
  334. APPROX_DBG( std::cout << " Self-intersection check failed" << std::endl );
  335. }
  336. }
  337. if( last == c_last_none )
  338. {
  339. if( first < 0 )
  340. jEndIdx = first + aSrc.PointCount();
  341. // Add point
  342. dst.Append( p0 );
  343. APPROX_DBG( std::cout << " Add pt " << VECTOR2I( p0 ) << std::endl );
  344. }
  345. }
  346. dst.SetClosed( true );
  347. // Try to merge arcs
  348. int iarc0 = dst.ArcIndex( 0 );
  349. int iarc1 = dst.ArcIndex( dst.GetSegmentCount() - 1 );
  350. if( iarc0 != -1 && iarc1 != -1 )
  351. {
  352. APPROX_DBG( std::cout << "Final arcs " << iarc0 << " " << iarc1 << std::endl );
  353. if( iarc0 == iarc1 )
  354. {
  355. SHAPE_ARC arc = dst.Arc( iarc0 );
  356. VECTOR2D p0 = arc.GetP0();
  357. VECTOR2D p1 = arc.GetP1();
  358. // If we have only one arc and the gap is small, make it a circle
  359. if( ( p1 - p0 ).EuclideanNorm() < c_circleCloseGap )
  360. {
  361. dst.Clear();
  362. dst.Append( SHAPE_ARC( arc.GetCenter(), arc.GetP0(), ANGLE_360 ) );
  363. }
  364. }
  365. else
  366. {
  367. // Merge first and last arcs if they are similar
  368. SHAPE_ARC arc0 = dst.Arc( iarc0 );
  369. SHAPE_ARC arc1 = dst.Arc( iarc1 );
  370. VECTOR2D ac0 = arc0.GetCenter();
  371. VECTOR2D ac1 = arc1.GetCenter();
  372. double ar0 = arc0.GetRadius();
  373. double ar1 = arc1.GetRadius();
  374. if( std::abs( ar0 - ar1 ) <= c_radiusDeviation
  375. && ( ac0 - ac1 ).EuclideanNorm() <= c_arcCenterDeviation )
  376. {
  377. dst.RemoveShape( 0 );
  378. dst.RemoveShape( -1 );
  379. SHAPE_ARC merged( arc1.GetP0(), arc1.GetArcMid(), arc0.GetP1(), 0 );
  380. dst.Append( merged );
  381. }
  382. }
  383. }
  384. return dst;
  385. }
  386. static TopoDS_Shape getOneShape( Handle( XCAFDoc_ShapeTool ) aShapeTool )
  387. {
  388. TDF_LabelSequence theLabels;
  389. aShapeTool->GetFreeShapes( theLabels );
  390. TopoDS_Shape aShape;
  391. if( theLabels.Length() == 1 )
  392. return aShapeTool->GetShape( theLabels.Value( 1 ) );
  393. TopoDS_Compound aCompound;
  394. BRep_Builder aBuilder;
  395. aBuilder.MakeCompound( aCompound );
  396. for( TDF_LabelSequence::Iterator anIt( theLabels ); anIt.More(); anIt.Next() )
  397. {
  398. TopoDS_Shape aFreeShape;
  399. if( !aShapeTool->GetShape( anIt.Value(), aFreeShape ) )
  400. continue;
  401. aBuilder.Add( aCompound, aFreeShape );
  402. }
  403. if( aCompound.NbChildren() > 0 )
  404. aShape = aCompound;
  405. return aShape;
  406. }
  407. // Apply scaling to shapes within theLabel.
  408. // Based on XCAFDoc_Editor::RescaleGeometry
  409. static Standard_Boolean rescaleShapes( const TDF_Label& theLabel, const gp_XYZ& aScale )
  410. {
  411. if( theLabel.IsNull() )
  412. {
  413. Message::SendFail( "Null label." );
  414. return Standard_False;
  415. }
  416. if( Abs( aScale.X() ) <= gp::Resolution() || Abs( aScale.Y() ) <= gp::Resolution()
  417. || Abs( aScale.Z() ) <= gp::Resolution() )
  418. {
  419. Message::SendFail( "Scale factor is too small." );
  420. return Standard_False;
  421. }
  422. Handle( XCAFDoc_ShapeTool ) aShapeTool = XCAFDoc_DocumentTool::ShapeTool( theLabel );
  423. if( aShapeTool.IsNull() )
  424. {
  425. Message::SendFail( "Couldn't find XCAFDoc_ShapeTool attribute." );
  426. return Standard_False;
  427. }
  428. Handle( KI_XCAFDoc_AssemblyGraph ) aG = new KI_XCAFDoc_AssemblyGraph( theLabel );
  429. if( aG.IsNull() )
  430. {
  431. Message::SendFail( "Couldn't create assembly graph." );
  432. return Standard_False;
  433. }
  434. Standard_Boolean anIsDone = Standard_True;
  435. // clang-format off
  436. gp_GTrsf aGTrsf;
  437. aGTrsf.SetVectorialPart( gp_Mat( aScale.X(), 0, 0,
  438. 0, aScale.Y(), 0,
  439. 0, 0, aScale.Z() ) );
  440. // clang-format on
  441. BRepBuilderAPI_GTransform aBRepTrsf( aGTrsf );
  442. for( Standard_Integer idx = 1; idx <= aG->NbNodes(); idx++ )
  443. {
  444. const KI_XCAFDoc_AssemblyGraph::NodeType aNodeType = aG->GetNodeType( idx );
  445. if( ( aNodeType != KI_XCAFDoc_AssemblyGraph::NodeType_Part )
  446. && ( aNodeType != KI_XCAFDoc_AssemblyGraph::NodeType_Occurrence ) )
  447. {
  448. continue;
  449. }
  450. const TDF_Label& aLabel = aG->GetNode( idx );
  451. if( aNodeType == KI_XCAFDoc_AssemblyGraph::NodeType_Part )
  452. {
  453. const TopoDS_Shape aShape = aShapeTool->GetShape( aLabel );
  454. aBRepTrsf.Perform( aShape, Standard_True );
  455. if( !aBRepTrsf.IsDone() )
  456. {
  457. Standard_SStream aSS;
  458. TCollection_AsciiString anEntry;
  459. TDF_Tool::Entry( aLabel, anEntry );
  460. aSS << "Shape " << anEntry << " is not scaled!";
  461. Message::SendFail( aSS.str().c_str() );
  462. anIsDone = Standard_False;
  463. return Standard_False;
  464. }
  465. TopoDS_Shape aScaledShape = aBRepTrsf.Shape();
  466. aShapeTool->SetShape( aLabel, aScaledShape );
  467. // Update sub-shapes
  468. TDF_LabelSequence aSubshapes;
  469. aShapeTool->GetSubShapes( aLabel, aSubshapes );
  470. for( TDF_LabelSequence::Iterator anItSs( aSubshapes ); anItSs.More(); anItSs.Next() )
  471. {
  472. const TDF_Label& aLSs = anItSs.Value();
  473. const TopoDS_Shape aSs = aShapeTool->GetShape( aLSs );
  474. const TopoDS_Shape aSs1 = aBRepTrsf.ModifiedShape( aSs );
  475. aShapeTool->SetShape( aLSs, aSs1 );
  476. }
  477. // These attributes will be recomputed eventually, but clear them just in case
  478. aLabel.ForgetAttribute( XCAFDoc_Area::GetID() );
  479. aLabel.ForgetAttribute( XCAFDoc_Centroid::GetID() );
  480. aLabel.ForgetAttribute( XCAFDoc_Volume::GetID() );
  481. }
  482. else if( aNodeType == KI_XCAFDoc_AssemblyGraph::NodeType_Occurrence )
  483. {
  484. TopLoc_Location aLoc = aShapeTool->GetLocation( aLabel );
  485. gp_Trsf aTrsf = aLoc.Transformation();
  486. aTrsf.SetTranslationPart( aTrsf.TranslationPart().Multiplied( aScale ) );
  487. XCAFDoc_Location::Set( aLabel, aTrsf );
  488. }
  489. }
  490. if( !anIsDone )
  491. {
  492. return Standard_False;
  493. }
  494. aShapeTool->UpdateAssemblies();
  495. return anIsDone;
  496. }
  497. static bool fuseShapes( auto& aInputShapes, TopoDS_Shape& aOutShape )
  498. {
  499. BRepAlgoAPI_Fuse mkFuse;
  500. TopTools_ListOfShape shapeArguments, shapeTools;
  501. for( TopoDS_Shape& sh : aInputShapes )
  502. {
  503. if( sh.IsNull() )
  504. continue;
  505. if( shapeArguments.IsEmpty() )
  506. shapeArguments.Append( sh );
  507. else
  508. shapeTools.Append( sh );
  509. }
  510. mkFuse.SetRunParallel( true );
  511. mkFuse.SetToFillHistory( false );
  512. mkFuse.SetArguments( shapeArguments );
  513. mkFuse.SetTools( shapeTools );
  514. mkFuse.Build();
  515. if( mkFuse.HasErrors() || mkFuse.HasWarnings() )
  516. {
  517. ReportMessage( _( "** Got problems while fusing shapes **\n" ) );
  518. if( mkFuse.HasErrors() )
  519. {
  520. ReportMessage( _( "Errors:\n" ) );
  521. mkFuse.DumpErrors( std::cout );
  522. }
  523. if( mkFuse.HasWarnings() )
  524. {
  525. ReportMessage( _( "Warnings:\n" ) );
  526. mkFuse.DumpWarnings( std::cout );
  527. }
  528. std::cout << "\n";
  529. }
  530. if( mkFuse.IsDone() )
  531. {
  532. TopoDS_Shape fusedShape = mkFuse.Shape();
  533. ShapeUpgrade_UnifySameDomain unify( fusedShape, true, true, false );
  534. unify.History() = nullptr;
  535. unify.Build();
  536. TopoDS_Shape unifiedShapes = unify.Shape();
  537. if( unifiedShapes.IsNull() )
  538. {
  539. ReportMessage( _( "** ShapeUpgrade_UnifySameDomain produced a null shape **\n" ) );
  540. }
  541. else
  542. {
  543. aOutShape = unifiedShapes;
  544. return true;
  545. }
  546. }
  547. return false;
  548. }
  549. static TopoDS_Compound makeCompound( auto& aInputShapes )
  550. {
  551. TopoDS_Compound compound;
  552. BRep_Builder builder;
  553. builder.MakeCompound( compound );
  554. for( TopoDS_Shape& shape : aInputShapes )
  555. builder.Add( compound, shape );
  556. return compound;
  557. }
  558. // Try to fuse shapes. If that fails, just add them to a compound
  559. static TopoDS_Shape fuseShapesOrCompound( TopTools_ListOfShape& aInputShapes )
  560. {
  561. TopoDS_Shape outShape;
  562. if( aInputShapes.Size() == 1 )
  563. return aInputShapes.First();
  564. if( fuseShapes( aInputShapes, outShape ) )
  565. return outShape;
  566. return makeCompound( aInputShapes );
  567. }
  568. // Sets names in assembly to <aPrefix> (<old name>), or to <aPrefix>
  569. static Standard_Boolean prefixNames( const TDF_Label& aLabel,
  570. const TCollection_ExtendedString& aPrefix )
  571. {
  572. Handle( KI_XCAFDoc_AssemblyGraph ) aG = new KI_XCAFDoc_AssemblyGraph( aLabel );
  573. if( aG.IsNull() )
  574. {
  575. Message::SendFail( "Couldn't create assembly graph." );
  576. return Standard_False;
  577. }
  578. Standard_Boolean anIsDone = Standard_True;
  579. for( Standard_Integer idx = 1; idx <= aG->NbNodes(); idx++ )
  580. {
  581. const TDF_Label& lbl = aG->GetNode( idx );
  582. Handle( TDataStd_Name ) nameHandle;
  583. if( lbl.FindAttribute( TDataStd_Name::GetID(), nameHandle ) )
  584. {
  585. TCollection_ExtendedString name;
  586. name += aPrefix;
  587. name += " (";
  588. name += nameHandle->Get();
  589. name += ")";
  590. TDataStd_Name::Set( lbl, name );
  591. }
  592. else
  593. {
  594. TDataStd_Name::Set( lbl, aPrefix );
  595. }
  596. }
  597. return anIsDone;
  598. }
  599. STEP_PCB_MODEL::STEP_PCB_MODEL( const wxString& aPcbName )
  600. {
  601. m_app = XCAFApp_Application::GetApplication();
  602. m_app->NewDocument( "MDTV-XCAF", m_doc );
  603. m_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
  604. m_assy_label = m_assy->NewShape();
  605. m_hasPCB = false;
  606. m_simplifyShapes = true;
  607. m_components = 0;
  608. m_precision = USER_PREC;
  609. m_angleprec = USER_ANGLE_PREC;
  610. m_mergeOCCMaxDist = OCC_MAX_DISTANCE_TO_MERGE_POINTS;
  611. m_minx = 1.0e10; // absurdly large number; any valid PCB X value will be smaller
  612. m_pcbName = aPcbName;
  613. m_maxError = pcbIUScale.mmToIU( ARC_TO_SEGMENT_MAX_ERROR_MM );
  614. m_fuseShapes = false;
  615. m_outFmt = OUTPUT_FORMAT::FMT_OUT_UNKNOWN;
  616. }
  617. STEP_PCB_MODEL::~STEP_PCB_MODEL()
  618. {
  619. if( m_doc->CanClose() == CDM_CCS_OK )
  620. m_doc->Close();
  621. }
  622. bool STEP_PCB_MODEL::AddPadShape( const PAD* aPad, const VECTOR2D& aOrigin, bool aVia )
  623. {
  624. bool success = true;
  625. std::vector<TopoDS_Shape> padShapes;
  626. for( PCB_LAYER_ID pcb_layer : aPad->GetLayerSet().Seq() )
  627. {
  628. if( !m_enabledLayers.Contains( pcb_layer ) )
  629. continue;
  630. if( pcb_layer == F_Mask || pcb_layer == B_Mask )
  631. continue;
  632. if( !aPad->FlashLayer( pcb_layer ) )
  633. continue;
  634. double Zpos, thickness;
  635. getLayerZPlacement( pcb_layer, Zpos, thickness );
  636. if( !aVia )
  637. {
  638. // Pad surface as a separate face for FEM simulations.
  639. if( pcb_layer == F_Cu )
  640. thickness += 0.005;
  641. else if( pcb_layer == B_Cu )
  642. thickness -= 0.005;
  643. }
  644. TopoDS_Shape testShape;
  645. // Make a shape on copper layers
  646. SHAPE_POLY_SET polySet;
  647. aPad->TransformShapeToPolygon( polySet, pcb_layer, 0, ARC_HIGH_DEF, ERROR_INSIDE );
  648. success &= MakeShapes( padShapes, polySet, m_simplifyShapes, thickness, Zpos, aOrigin );
  649. if( testShape.IsNull() )
  650. {
  651. std::vector<TopoDS_Shape> testShapes;
  652. MakeShapes( testShapes, polySet, m_simplifyShapes, 0.0, Zpos + thickness, aOrigin );
  653. if( testShapes.size() > 0 )
  654. testShape = testShapes.front();
  655. }
  656. if( !aVia && !testShape.IsNull() )
  657. {
  658. if( pcb_layer == F_Cu || pcb_layer == B_Cu )
  659. {
  660. wxString name;
  661. name << "Pad_";
  662. if( pcb_layer == F_Cu )
  663. name << 'F' << '_';
  664. else if( pcb_layer == B_Cu )
  665. name << 'B' << '_';
  666. name << aPad->GetParentFootprint()->GetReferenceAsString() << '_'
  667. << aPad->GetNumber() << '_' << aPad->GetShortNetname();
  668. gp_Pnt point( pcbIUScale.IUTomm( aPad->GetX() - aOrigin.x ),
  669. -pcbIUScale.IUTomm( aPad->GetY() - aOrigin.y ), Zpos + thickness );
  670. m_pad_points[name] = { point, testShape };
  671. }
  672. }
  673. }
  674. if( aPad->GetAttribute() == PAD_ATTRIB::PTH && aPad->IsOnLayer( F_Cu )
  675. && aPad->IsOnLayer( B_Cu ) )
  676. {
  677. double f_pos, f_thickness;
  678. double b_pos, b_thickness;
  679. getLayerZPlacement( F_Cu, f_pos, f_thickness );
  680. getLayerZPlacement( B_Cu, b_pos, b_thickness );
  681. double top = std::max( f_pos, f_pos + f_thickness );
  682. double bottom = std::min( b_pos, b_pos + b_thickness );
  683. TopoDS_Shape plating;
  684. std::shared_ptr<SHAPE_SEGMENT> seg_hole = aPad->GetEffectiveHoleShape();
  685. double width = std::min( aPad->GetDrillSize().x, aPad->GetDrillSize().y );
  686. if( MakeShapeAsThickSegment( plating, seg_hole->GetSeg().A, seg_hole->GetSeg().B, width,
  687. ( top - bottom ), bottom, aOrigin ) )
  688. {
  689. padShapes.push_back( plating );
  690. }
  691. else
  692. {
  693. success = false;
  694. }
  695. }
  696. if( !success ) // Error
  697. ReportMessage( wxT( "OCC error adding pad/via polygon.\n" ) );
  698. // Fuse pad shapes here before fusing them with tracks because OCCT sometimes has trouble
  699. if( m_fuseShapes )
  700. {
  701. TopTools_ListOfShape padShapesList;
  702. for( const TopoDS_Shape& shape : padShapes )
  703. padShapesList.Append( shape );
  704. m_board_copper_pads.push_back( fuseShapesOrCompound( padShapesList ) );
  705. }
  706. else
  707. {
  708. for( const TopoDS_Shape& shape : padShapes )
  709. m_board_copper_pads.push_back( shape );
  710. }
  711. return success;
  712. }
  713. bool STEP_PCB_MODEL::AddHole( const SHAPE_SEGMENT& aShape, int aPlatingThickness,
  714. PCB_LAYER_ID aLayerTop, PCB_LAYER_ID aLayerBot, bool aVia,
  715. const VECTOR2D& aOrigin )
  716. {
  717. double margin = 0.001; // a small margin on the Z axix to be sure the hole
  718. // is bigger than the board with copper
  719. // must be > OCC_MAX_DISTANCE_TO_MERGE_POINTS
  720. // Pads are taller by 0.01 mm
  721. if( !aVia )
  722. margin += 0.01;
  723. double f_pos, f_thickness;
  724. double b_pos, b_thickness;
  725. getLayerZPlacement( aLayerTop, f_pos, f_thickness );
  726. getLayerZPlacement( aLayerBot, b_pos, b_thickness );
  727. double top = std::max( f_pos, f_pos + f_thickness );
  728. double bottom = std::min( b_pos, b_pos + b_thickness );
  729. double holeZsize = ( top - bottom ) + ( margin * 2 );
  730. double boardDrill = aShape.GetWidth();
  731. double copperDrill = boardDrill - aPlatingThickness * 2;
  732. TopoDS_Shape copperHole, boardHole;
  733. if( MakeShapeAsThickSegment( copperHole, aShape.GetSeg().A, aShape.GetSeg().B, copperDrill,
  734. holeZsize, bottom - margin, aOrigin ) )
  735. {
  736. m_copperCutouts.push_back( copperHole );
  737. }
  738. else
  739. {
  740. return false;
  741. }
  742. if( MakeShapeAsThickSegment( boardHole, aShape.GetSeg().A, aShape.GetSeg().B, boardDrill,
  743. holeZsize, bottom - margin, aOrigin ) )
  744. {
  745. m_boardCutouts.push_back( boardHole );
  746. }
  747. else
  748. {
  749. return false;
  750. }
  751. return true;
  752. }
  753. bool STEP_PCB_MODEL::AddBarrel( const SHAPE_SEGMENT& aShape, PCB_LAYER_ID aLayerTop,
  754. PCB_LAYER_ID aLayerBot, bool aVia, const VECTOR2D& aOrigin )
  755. {
  756. double f_pos, f_thickness;
  757. double b_pos, b_thickness;
  758. getLayerZPlacement( aLayerTop, f_pos, f_thickness );
  759. getLayerZPlacement( aLayerBot, b_pos, b_thickness );
  760. double top = std::max( f_pos, f_pos + f_thickness );
  761. double bottom = std::min( b_pos, b_pos + b_thickness );
  762. TopoDS_Shape plating;
  763. if( !MakeShapeAsThickSegment( plating, aShape.GetSeg().A, aShape.GetSeg().B, aShape.GetWidth(),
  764. ( top - bottom ), bottom, aOrigin ) )
  765. {
  766. return false;
  767. }
  768. if( aVia )
  769. m_board_copper_vias.push_back( plating );
  770. else
  771. m_board_copper_pads.push_back( plating );
  772. return true;
  773. }
  774. void STEP_PCB_MODEL::getLayerZPlacement( const PCB_LAYER_ID aLayer, double& aZPos,
  775. double& aThickness )
  776. {
  777. // Offsets above copper in mm
  778. static const double c_silkscreenAboveCopper = 0.04;
  779. static const double c_soldermaskAboveCopper = 0.015;
  780. if( IsCopperLayer( aLayer ) )
  781. {
  782. getCopperLayerZPlacement( aLayer, aZPos, aThickness );
  783. }
  784. else if( IsFrontLayer( aLayer ) )
  785. {
  786. double f_pos, f_thickness;
  787. getCopperLayerZPlacement( F_Cu, f_pos, f_thickness );
  788. double top = std::max( f_pos, f_pos + f_thickness );
  789. if( aLayer == F_SilkS )
  790. aZPos = top + c_silkscreenAboveCopper;
  791. else
  792. aZPos = top + c_soldermaskAboveCopper;
  793. aThickness = 0.0; // Normal points up
  794. }
  795. else if( IsBackLayer( aLayer ) )
  796. {
  797. double b_pos, b_thickness;
  798. getCopperLayerZPlacement( B_Cu, b_pos, b_thickness );
  799. double bottom = std::min( b_pos, b_pos + b_thickness );
  800. if( aLayer == B_SilkS )
  801. aZPos = bottom - c_silkscreenAboveCopper;
  802. else
  803. aZPos = bottom - c_soldermaskAboveCopper;
  804. aThickness = -0.0; // Normal points down
  805. }
  806. }
  807. void STEP_PCB_MODEL::getCopperLayerZPlacement( const PCB_LAYER_ID aLayer, double& aZPos,
  808. double& aThickness )
  809. {
  810. int z = 0;
  811. int thickness = 0;
  812. bool wasPrepreg = false;
  813. const std::vector<BOARD_STACKUP_ITEM*>& materials = m_stackup.GetList();
  814. // Iterate from bottom to top
  815. for( auto it = materials.rbegin(); it != materials.rend(); ++it )
  816. {
  817. const BOARD_STACKUP_ITEM* item = *it;
  818. if( item->GetType() == BS_ITEM_TYPE_COPPER )
  819. {
  820. if( aLayer == B_Cu )
  821. {
  822. // This is the first encountered layer
  823. thickness = -item->GetThickness();
  824. break;
  825. }
  826. // Inner copper position is usually inside prepreg
  827. if( wasPrepreg && item->GetBrdLayerId() != F_Cu )
  828. {
  829. z += item->GetThickness();
  830. thickness = -item->GetThickness();
  831. }
  832. else
  833. {
  834. thickness = item->GetThickness();
  835. }
  836. if( item->GetBrdLayerId() == aLayer )
  837. break;
  838. if( !wasPrepreg && item->GetBrdLayerId() != B_Cu )
  839. z += item->GetThickness();
  840. }
  841. else if( item->GetType() == BS_ITEM_TYPE_DIELECTRIC )
  842. {
  843. wasPrepreg = ( item->GetTypeName() == KEY_PREPREG );
  844. // Dielectric can have sub-layers. Layer 0 is the main layer
  845. // Not frequent, but possible
  846. thickness = 0;
  847. for( int idx = 0; idx < item->GetSublayersCount(); idx++ )
  848. thickness += item->GetThickness( idx );
  849. z += thickness;
  850. }
  851. }
  852. aZPos = pcbIUScale.IUTomm( z );
  853. aThickness = pcbIUScale.IUTomm( thickness );
  854. }
  855. void STEP_PCB_MODEL::getBoardBodyZPlacement( double& aZPos, double& aThickness )
  856. {
  857. double f_pos, f_thickness;
  858. double b_pos, b_thickness;
  859. getLayerZPlacement( F_Cu, f_pos, f_thickness );
  860. getLayerZPlacement( B_Cu, b_pos, b_thickness );
  861. double top = std::min( f_pos, f_pos + f_thickness );
  862. double bottom = std::max( b_pos, b_pos + b_thickness );
  863. aThickness = ( top - bottom );
  864. aZPos = bottom;
  865. wxASSERT( aZPos == 0.0 );
  866. }
  867. bool STEP_PCB_MODEL::AddPolygonShapes( const SHAPE_POLY_SET* aPolyShapes, PCB_LAYER_ID aLayer,
  868. const VECTOR2D& aOrigin )
  869. {
  870. bool success = true;
  871. if( aPolyShapes->IsEmpty() )
  872. return true;
  873. if( !m_enabledLayers.Contains( aLayer ) )
  874. return true;
  875. double z_pos, thickness;
  876. getLayerZPlacement( aLayer, z_pos, thickness );
  877. std::vector<TopoDS_Shape>& targetVec = IsCopperLayer( aLayer ) ? m_board_copper
  878. : aLayer == F_SilkS || aLayer == B_SilkS
  879. ? m_board_silkscreen
  880. : m_board_soldermask;
  881. if( !MakeShapes( targetVec, *aPolyShapes, m_simplifyShapes, thickness, z_pos, aOrigin ) )
  882. {
  883. ReportMessage(
  884. wxString::Format( wxT( "Could not add shape (%d points) to copper layer on %s.\n" ),
  885. aPolyShapes->FullPointCount(), LayerName( aLayer ) ) );
  886. success = false;
  887. }
  888. return success;
  889. }
  890. bool STEP_PCB_MODEL::AddComponent( const std::string& aFileNameUTF8, const std::string& aRefDes,
  891. bool aBottom, VECTOR2D aPosition, double aRotation, VECTOR3D aOffset,
  892. VECTOR3D aOrientation, VECTOR3D aScale, bool aSubstituteModels )
  893. {
  894. if( aFileNameUTF8.empty() )
  895. {
  896. ReportMessage( wxString::Format( wxT( "No model defined for component %s.\n" ), aRefDes ) );
  897. return false;
  898. }
  899. wxString fileName( wxString::FromUTF8( aFileNameUTF8.c_str() ) );
  900. ReportMessage( wxString::Format( wxT( "Adding component %s.\n" ), aRefDes ) );
  901. // first retrieve a label
  902. TDF_Label lmodel;
  903. wxString errorMessage;
  904. if( !getModelLabel( aFileNameUTF8, aScale, lmodel, aSubstituteModels, &errorMessage ) )
  905. {
  906. if( errorMessage.IsEmpty() )
  907. ReportMessage( wxString::Format( wxT( "No model for filename '%s'.\n" ), fileName ) );
  908. else
  909. ReportMessage( errorMessage );
  910. return false;
  911. }
  912. // calculate the Location transform
  913. TopLoc_Location toploc;
  914. if( !getModelLocation( aBottom, aPosition, aRotation, aOffset, aOrientation, toploc ) )
  915. {
  916. ReportMessage(
  917. wxString::Format( wxT( "No location data for filename '%s'.\n" ), fileName ) );
  918. return false;
  919. }
  920. // add the located sub-assembly
  921. TDF_Label llabel = m_assy->AddComponent( m_assy_label, lmodel, toploc );
  922. if( llabel.IsNull() )
  923. {
  924. ReportMessage( wxString::Format( wxT( "Could not add component with filename '%s'.\n" ),
  925. fileName ) );
  926. return false;
  927. }
  928. // attach the RefDes name
  929. TCollection_ExtendedString refdes( aRefDes.c_str() );
  930. TDataStd_Name::Set( llabel, refdes );
  931. return true;
  932. }
  933. void STEP_PCB_MODEL::SetEnabledLayers( const LSET& aLayers )
  934. {
  935. m_enabledLayers = aLayers;
  936. }
  937. void STEP_PCB_MODEL::SetFuseShapes( bool aValue )
  938. {
  939. m_fuseShapes = aValue;
  940. }
  941. void STEP_PCB_MODEL::SetSimplifyShapes( bool aValue )
  942. {
  943. m_simplifyShapes = aValue;
  944. }
  945. void STEP_PCB_MODEL::SetStackup( const BOARD_STACKUP& aStackup )
  946. {
  947. m_stackup = aStackup;
  948. }
  949. void STEP_PCB_MODEL::SetNetFilter( const wxString& aFilter )
  950. {
  951. m_netFilter = aFilter;
  952. }
  953. void STEP_PCB_MODEL::SetCopperColor( double r, double g, double b )
  954. {
  955. m_copperColor[0] = r;
  956. m_copperColor[1] = g;
  957. m_copperColor[2] = b;
  958. }
  959. void STEP_PCB_MODEL::SetPadColor( double r, double g, double b )
  960. {
  961. m_padColor[0] = r;
  962. m_padColor[1] = g;
  963. m_padColor[2] = b;
  964. }
  965. void STEP_PCB_MODEL::OCCSetMergeMaxDistance( double aDistance )
  966. {
  967. // Ensure a minimal value (in mm)
  968. m_mergeOCCMaxDist = aDistance;
  969. }
  970. bool STEP_PCB_MODEL::isBoardOutlineValid()
  971. {
  972. return m_pcb_labels.size() > 0;
  973. }
  974. bool STEP_PCB_MODEL::MakeShapeAsThickSegment( TopoDS_Shape& aShape,
  975. VECTOR2D aStartPoint, VECTOR2D aEndPoint,
  976. double aWidth, double aThickness,
  977. double aZposition, const VECTOR2D& aOrigin )
  978. {
  979. // make a wide segment from 2 lines and 2 180 deg arcs
  980. // We need 6 points (3 per arcs)
  981. VECTOR2D coords[6];
  982. // We build a horizontal segment, and after rotate it
  983. double len = ( aEndPoint - aStartPoint ).EuclideanNorm();
  984. double h_width = aWidth/2.0;
  985. // First is end point of first arc, and also start point of first line
  986. coords[0] = VECTOR2D{ 0.0, h_width };
  987. // end point of first line and start point of second arc
  988. coords[1] = VECTOR2D{ len, h_width };
  989. // middle point of second arc
  990. coords[2] = VECTOR2D{ len + h_width, 0.0 };
  991. // start point of second line and end point of second arc
  992. coords[3] = VECTOR2D{ len, -h_width };
  993. // end point of second line and start point of first arc
  994. coords[4] = VECTOR2D{ 0, -h_width };
  995. // middle point of first arc
  996. coords[5] = VECTOR2D{ -h_width, 0.0 };
  997. // Rotate and move to segment position
  998. EDA_ANGLE seg_angle( aEndPoint - aStartPoint );
  999. for( int ii = 0; ii < 6; ii++ )
  1000. {
  1001. RotatePoint( coords[ii], VECTOR2D{ 0, 0 }, -seg_angle ),
  1002. coords[ii] += aStartPoint;
  1003. }
  1004. // Convert to 3D points
  1005. gp_Pnt coords3D[ 6 ];
  1006. for( int ii = 0; ii < 6; ii++ )
  1007. {
  1008. coords3D[ii] = gp_Pnt( pcbIUScale.IUTomm( coords[ii].x - aOrigin.x ),
  1009. -pcbIUScale.IUTomm( coords[ii].y - aOrigin.y ), aZposition );
  1010. }
  1011. // Build OpenCascade shape outlines
  1012. BRepBuilderAPI_MakeWire wire;
  1013. bool success = true;
  1014. // Short segments (distance between end points < m_mergeOCCMaxDist(in mm)) must be
  1015. // skipped because OCC merge end points, and a null shape is created
  1016. bool short_seg = pcbIUScale.IUTomm( len ) <= m_mergeOCCMaxDist;
  1017. try
  1018. {
  1019. TopoDS_Edge edge;
  1020. if( short_seg )
  1021. {
  1022. Handle( Geom_Circle ) circle = GC_MakeCircle( coords3D[1], // arc1 start point
  1023. coords3D[2], // arc1 mid point
  1024. coords3D[5] // arc2 mid point
  1025. );
  1026. edge = BRepBuilderAPI_MakeEdge( circle );
  1027. wire.Add( edge );
  1028. }
  1029. else
  1030. {
  1031. edge = BRepBuilderAPI_MakeEdge( coords3D[0], coords3D[1] );
  1032. wire.Add( edge );
  1033. Handle( Geom_TrimmedCurve ) arcOfCircle =
  1034. GC_MakeArcOfCircle( coords3D[1], // start point
  1035. coords3D[2], // mid point
  1036. coords3D[3] // end point
  1037. );
  1038. edge = BRepBuilderAPI_MakeEdge( arcOfCircle );
  1039. wire.Add( edge );
  1040. edge = BRepBuilderAPI_MakeEdge( coords3D[3], coords3D[4] );
  1041. wire.Add( edge );
  1042. Handle( Geom_TrimmedCurve ) arcOfCircle2 =
  1043. GC_MakeArcOfCircle( coords3D[4], // start point
  1044. coords3D[5], // mid point
  1045. coords3D[0] // end point
  1046. );
  1047. edge = BRepBuilderAPI_MakeEdge( arcOfCircle2 );
  1048. wire.Add( edge );
  1049. }
  1050. }
  1051. catch( const Standard_Failure& e )
  1052. {
  1053. ReportMessage( wxString::Format( wxT( "build shape segment: OCC exception: %s\n" ),
  1054. e.GetMessageString() ) );
  1055. return false;
  1056. }
  1057. BRepBuilderAPI_MakeFace face;
  1058. try
  1059. {
  1060. gp_Pln plane( coords3D[0], gp::DZ() );
  1061. face = BRepBuilderAPI_MakeFace( plane, wire );
  1062. }
  1063. catch( const Standard_Failure& e )
  1064. {
  1065. ReportMessage( wxString::Format( wxT( "MakeShapeThickSegment: OCC exception: %s\n" ),
  1066. e.GetMessageString() ) );
  1067. return false;
  1068. }
  1069. if( aThickness != 0.0 )
  1070. {
  1071. aShape = BRepPrimAPI_MakePrism( face, gp_Vec( 0, 0, aThickness ) );
  1072. if( aShape.IsNull() )
  1073. {
  1074. ReportMessage( wxT( "failed to create a prismatic shape\n" ) );
  1075. return false;
  1076. }
  1077. }
  1078. else
  1079. {
  1080. aShape = face;
  1081. }
  1082. return success;
  1083. }
  1084. static wxString formatBBox( const BOX2I& aBBox )
  1085. {
  1086. wxString str;
  1087. UNITS_PROVIDER up( pcbIUScale, EDA_UNITS::MILLIMETRES );
  1088. str << "x0: " << up.StringFromValue( aBBox.GetLeft(), false ) << "; ";
  1089. str << "y0: " << up.StringFromValue( aBBox.GetTop(), false ) << "; ";
  1090. str << "x1: " << up.StringFromValue( aBBox.GetRight(), false ) << "; ";
  1091. str << "y1: " << up.StringFromValue( aBBox.GetBottom(), false );
  1092. return str;
  1093. }
  1094. static bool makeWireFromChain( BRepLib_MakeWire& aMkWire, const SHAPE_LINE_CHAIN& aChain,
  1095. double aMergeOCCMaxDist, double aZposition, const VECTOR2D& aOrigin )
  1096. {
  1097. auto toPoint = [&]( const VECTOR2D& aKiCoords ) -> gp_Pnt
  1098. {
  1099. return gp_Pnt( pcbIUScale.IUTomm( aKiCoords.x - aOrigin.x ),
  1100. -pcbIUScale.IUTomm( aKiCoords.y - aOrigin.y ), aZposition );
  1101. };
  1102. try
  1103. {
  1104. auto addSegment = [&]( const VECTOR2I& aPt0, const VECTOR2I& aPt1 ) -> bool
  1105. {
  1106. if( aPt0 == aPt1 )
  1107. return false;
  1108. gp_Pnt start = toPoint( aPt0 );
  1109. gp_Pnt end = toPoint( aPt1 );
  1110. BRepBuilderAPI_MakeEdge mkEdge( start, end );
  1111. if( !mkEdge.IsDone() || mkEdge.Edge().IsNull() )
  1112. {
  1113. ReportMessage( wxString::Format( wxT( "failed to make segment edge at (%d "
  1114. "%d) -> (%d %d), skipping\n" ),
  1115. aPt0.x, aPt0.y, aPt1.x, aPt1.y ) );
  1116. }
  1117. else
  1118. {
  1119. aMkWire.Add( mkEdge.Edge() );
  1120. if( aMkWire.Error() != BRepLib_WireDone )
  1121. {
  1122. ReportMessage( wxString::Format( wxT( "failed to add segment edge "
  1123. "at (%d %d) -> (%d %d)\n" ),
  1124. aPt0.x, aPt0.y, aPt1.x, aPt1.y ) );
  1125. return false;
  1126. }
  1127. }
  1128. return true;
  1129. };
  1130. auto addArc = [&]( const VECTOR2I& aPt0, const SHAPE_ARC& aArc ) -> bool
  1131. {
  1132. // Do not export too short segments: they create broken shape because OCC thinks
  1133. Handle( Geom_Curve ) curve;
  1134. if( aArc.GetCentralAngle() == ANGLE_360 )
  1135. {
  1136. gp_Ax2 axis = gp::XOY();
  1137. axis.SetLocation( toPoint( aArc.GetCenter() ) );
  1138. curve = GC_MakeCircle( axis, pcbIUScale.IUTomm( aArc.GetRadius() ) ).Value();
  1139. }
  1140. else
  1141. {
  1142. curve = GC_MakeArcOfCircle( toPoint( aPt0 ), toPoint( aArc.GetArcMid() ),
  1143. toPoint( aArc.GetP1() ) )
  1144. .Value();
  1145. }
  1146. if( curve.IsNull() )
  1147. return false;
  1148. aMkWire.Add( BRepBuilderAPI_MakeEdge( curve ) );
  1149. if( !aMkWire.IsDone() )
  1150. {
  1151. ReportMessage( wxString::Format(
  1152. wxT( "failed to add arc curve from (%d %d), arc p0 "
  1153. "(%d %d), mid (%d %d), p1 (%d %d)\n" ),
  1154. aPt0.x, aPt0.y, aArc.GetP0().x, aArc.GetP0().y, aArc.GetArcMid().x,
  1155. aArc.GetArcMid().y, aArc.GetP1().x, aArc.GetP1().y ) );
  1156. return false;
  1157. }
  1158. return true;
  1159. };
  1160. VECTOR2I firstPt;
  1161. VECTOR2I lastPt;
  1162. bool isFirstShape = true;
  1163. for( int i = 0; i <= aChain.PointCount() && i != -1; i = aChain.NextShape( i ) )
  1164. {
  1165. if( i == 0 )
  1166. {
  1167. if( aChain.IsArcSegment( 0 ) && aChain.IsArcSegment( aChain.PointCount() - 1 )
  1168. && aChain.ArcIndex( 0 ) == aChain.ArcIndex( aChain.PointCount() - 1 ) )
  1169. {
  1170. // Skip first arc (we should encounter it later)
  1171. int nextShape = aChain.NextShape( i );
  1172. // If nextShape points to the end, then we have a circle.
  1173. if( nextShape != -1 )
  1174. i = nextShape;
  1175. }
  1176. }
  1177. if( isFirstShape )
  1178. lastPt = aChain.CPoint( i );
  1179. bool isArc = aChain.IsArcSegment( i );
  1180. if( aChain.IsArcStart( i ) )
  1181. {
  1182. const SHAPE_ARC& currentArc = aChain.Arc( aChain.ArcIndex( i ) );
  1183. if( isFirstShape )
  1184. {
  1185. firstPt = currentArc.GetP0();
  1186. lastPt = firstPt;
  1187. }
  1188. if( addSegment( lastPt, currentArc.GetP0() ) )
  1189. lastPt = currentArc.GetP0();
  1190. if( addArc( lastPt, currentArc ) )
  1191. lastPt = currentArc.GetP1();
  1192. }
  1193. else if( !isArc )
  1194. {
  1195. const SEG& seg = aChain.CSegment( i );
  1196. if( isFirstShape )
  1197. {
  1198. firstPt = seg.A;
  1199. lastPt = firstPt;
  1200. }
  1201. if( addSegment( lastPt, seg.A ) )
  1202. lastPt = seg.A;
  1203. if( addSegment( lastPt, seg.B ) )
  1204. lastPt = seg.B;
  1205. }
  1206. isFirstShape = false;
  1207. }
  1208. if( lastPt != firstPt && !addSegment( lastPt, firstPt ) )
  1209. {
  1210. ReportMessage(
  1211. wxString::Format( wxT( "** Failed to close wire at %d, %d -> %d, %d **\n" ),
  1212. lastPt.x, lastPt.y, firstPt.x, firstPt.y ) );
  1213. return false;
  1214. }
  1215. }
  1216. catch( const Standard_Failure& e )
  1217. {
  1218. ReportMessage( wxString::Format( wxT( "makeWireFromChain: OCC exception: %s\n" ),
  1219. e.GetMessageString() ) );
  1220. return false;
  1221. }
  1222. return true;
  1223. }
  1224. bool STEP_PCB_MODEL::MakeShapes( std::vector<TopoDS_Shape>& aShapes, const SHAPE_POLY_SET& aPolySet, bool aConvertToArcs,
  1225. double aThickness, double aZposition, const VECTOR2D& aOrigin )
  1226. {
  1227. SHAPE_POLY_SET workingPoly = aPolySet;
  1228. workingPoly.Simplify( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE );
  1229. SHAPE_POLY_SET fallbackPoly = workingPoly;
  1230. if( aConvertToArcs )
  1231. {
  1232. SHAPE_POLY_SET approximated = workingPoly;
  1233. for( size_t polyId = 0; polyId < approximated.CPolygons().size(); polyId++ )
  1234. {
  1235. SHAPE_POLY_SET::POLYGON& polygon = approximated.Polygon( polyId );
  1236. for( size_t contId = 0; contId < polygon.size(); contId++ )
  1237. {
  1238. SHAPE_LINE_CHAIN approxChain = approximateLineChainWithArcs( polygon[contId] );
  1239. polygon[contId] = approxChain;
  1240. }
  1241. }
  1242. fallbackPoly = workingPoly;
  1243. workingPoly = approximated;
  1244. // TODO: this is not accurate because it doesn't check arcs.
  1245. /*if( approximated.IsSelfIntersecting() )
  1246. {
  1247. ReportMessage( wxString::Format( _( "\nApproximated polygon self-intersection check "
  1248. "failed\n" ) ) );
  1249. ReportMessage( wxString::Format( _( "z: %g; bounding box: %s\n" ), aZposition,
  1250. formatBBox( workingPoly.BBox() ) ) );
  1251. }
  1252. else
  1253. {
  1254. fallbackPoly = workingPoly;
  1255. workingPoly = approximated;
  1256. }*/
  1257. }
  1258. #if 0 // No longer in use
  1259. auto toPoint = [&]( const VECTOR2D& aKiCoords ) -> gp_Pnt
  1260. {
  1261. return gp_Pnt( pcbIUScale.IUTomm( aKiCoords.x - aOrigin.x ),
  1262. -pcbIUScale.IUTomm( aKiCoords.y - aOrigin.y ), aZposition );
  1263. };
  1264. #endif
  1265. gp_Pln basePlane( gp_Pnt( 0.0, 0.0, aZposition ),
  1266. std::signbit( aThickness ) ? -gp::DZ() : gp::DZ() );
  1267. for( size_t polyId = 0; polyId < workingPoly.CPolygons().size(); polyId++ )
  1268. {
  1269. SHAPE_POLY_SET::POLYGON& polygon = workingPoly.Polygon( polyId );
  1270. auto tryMakeWire = [this, &aZposition,
  1271. &aOrigin]( const SHAPE_LINE_CHAIN& aContour ) -> TopoDS_Wire
  1272. {
  1273. TopoDS_Wire wire;
  1274. BRepLib_MakeWire mkWire;
  1275. makeWireFromChain( mkWire, aContour, m_mergeOCCMaxDist, aZposition, aOrigin );
  1276. if( mkWire.IsDone() )
  1277. {
  1278. wire = mkWire.Wire();
  1279. }
  1280. else
  1281. {
  1282. ReportMessage(
  1283. wxString::Format( _( "Wire not done (contour points %d): OCC error %d\n" ),
  1284. static_cast<int>( aContour.PointCount() ),
  1285. static_cast<int>( mkWire.Error() ) ) );
  1286. ReportMessage( wxString::Format( _( "z: %g; bounding box: %s\n" ), aZposition,
  1287. formatBBox( aContour.BBox() ) ) );
  1288. }
  1289. if( !wire.IsNull() )
  1290. {
  1291. BRepAlgoAPI_Check check( wire, false, true );
  1292. if( !check.IsValid() )
  1293. {
  1294. ReportMessage( wxString::Format( _( "\nWire self-interference check "
  1295. "failed\n" ) ) );
  1296. ReportMessage( wxString::Format( _( "z: %g; bounding box: %s\n" ), aZposition,
  1297. formatBBox( aContour.BBox() ) ) );
  1298. wire.Nullify();
  1299. }
  1300. }
  1301. return wire;
  1302. };
  1303. BRepBuilderAPI_MakeFace mkFace;
  1304. for( size_t contId = 0; contId < polygon.size(); contId++ )
  1305. {
  1306. try
  1307. {
  1308. TopoDS_Wire wire = tryMakeWire( polygon[contId] );
  1309. if( aConvertToArcs && wire.IsNull() )
  1310. {
  1311. ReportMessage( wxString::Format( _( "Using non-simplified polygon.\n" ) ) );
  1312. // Fall back to original shape
  1313. wire = tryMakeWire( fallbackPoly.CPolygon( polyId )[contId] );
  1314. }
  1315. if( contId == 0 ) // Outline
  1316. {
  1317. if( !wire.IsNull() )
  1318. {
  1319. if( basePlane.Axis().Direction().Z() < 0 )
  1320. wire.Reverse();
  1321. mkFace = BRepBuilderAPI_MakeFace( basePlane, wire );
  1322. }
  1323. else
  1324. {
  1325. ReportMessage( wxString::Format( wxT( "\n** Outline skipped **\n" ) ) );
  1326. ReportMessage( wxString::Format( wxT( "z: %g; bounding box: %s\n" ),
  1327. aZposition,
  1328. formatBBox( polygon[contId].BBox() ) ) );
  1329. break;
  1330. }
  1331. }
  1332. else // Hole
  1333. {
  1334. if( !wire.IsNull() )
  1335. {
  1336. if( basePlane.Axis().Direction().Z() > 0 )
  1337. wire.Reverse();
  1338. mkFace.Add( wire );
  1339. }
  1340. else
  1341. {
  1342. ReportMessage( wxString::Format( wxT( "\n** Hole skipped **\n" ) ) );
  1343. ReportMessage( wxString::Format( wxT( "z: %g; bounding box: %s\n" ),
  1344. aZposition,
  1345. formatBBox( polygon[contId].BBox() ) ) );
  1346. }
  1347. }
  1348. }
  1349. catch( const Standard_Failure& e )
  1350. {
  1351. ReportMessage(
  1352. wxString::Format( wxT( "MakeShapes (contour %d): OCC exception: %s\n" ),
  1353. static_cast<int>( contId ), e.GetMessageString() ) );
  1354. return false;
  1355. }
  1356. }
  1357. if( mkFace.IsDone() )
  1358. {
  1359. TopoDS_Shape faceShape = mkFace.Shape();
  1360. if( aThickness != 0.0 )
  1361. {
  1362. TopoDS_Shape prism = BRepPrimAPI_MakePrism( faceShape, gp_Vec( 0, 0, aThickness ) );
  1363. aShapes.push_back( prism );
  1364. if( prism.IsNull() )
  1365. {
  1366. ReportMessage( _( "Failed to create a prismatic shape\n" ) );
  1367. return false;
  1368. }
  1369. }
  1370. else
  1371. {
  1372. aShapes.push_back( faceShape );
  1373. }
  1374. }
  1375. else
  1376. {
  1377. ReportMessage( wxString::Format( _( "** Face skipped **\n" ) ) );
  1378. }
  1379. }
  1380. return true;
  1381. }
  1382. // These colors are based on 3D viewer's colors and are different to "gbrjobColors"
  1383. static std::vector<FAB_LAYER_COLOR> s_soldermaskColors = {
  1384. { NotSpecifiedPrm(), wxColor( 20, 51, 36 ) }, // Not specified, not in .gbrjob file
  1385. { _HKI( "Green" ), wxColor( 20, 51, 36 ) }, // used in .gbrjob file
  1386. { _HKI( "Red" ), wxColor( 181, 19, 21 ) }, // used in .gbrjob file
  1387. { _HKI( "Blue" ), wxColor( 2, 59, 162 ) }, // used in .gbrjob file
  1388. { _HKI( "Purple" ), wxColor( 32, 2, 53 ) }, // used in .gbrjob file
  1389. { _HKI( "Black" ), wxColor( 11, 11, 11 ) }, // used in .gbrjob file
  1390. { _HKI( "White" ), wxColor( 245, 245, 245 ) }, // used in .gbrjob file
  1391. { _HKI( "Yellow" ), wxColor( 194, 195, 0 ) }, // used in .gbrjob file
  1392. { _HKI( "User defined" ), wxColor( 128, 128, 128 ) } // Free; the name is a dummy name here
  1393. };
  1394. static bool colorFromStackup( BOARD_STACKUP_ITEM_TYPE aType, const wxString& aColorStr,
  1395. COLOR4D& aColorOut )
  1396. {
  1397. if( !IsPrmSpecified( aColorStr ) )
  1398. return false;
  1399. if( aColorStr.StartsWith( wxT( "#" ) ) ) // User defined color
  1400. {
  1401. aColorOut = COLOR4D( aColorStr );
  1402. return true;
  1403. }
  1404. else
  1405. {
  1406. const std::vector<FAB_LAYER_COLOR>& colors =
  1407. ( aType == BS_ITEM_TYPE_SOLDERMASK || aType == BS_ITEM_TYPE_SILKSCREEN )
  1408. ? s_soldermaskColors
  1409. : GetStandardColors( aType );
  1410. for( const FAB_LAYER_COLOR& fabColor : colors )
  1411. {
  1412. if( fabColor.GetName() == aColorStr )
  1413. {
  1414. aColorOut = fabColor.GetColor( aType );
  1415. return true;
  1416. }
  1417. }
  1418. }
  1419. return false;
  1420. }
  1421. bool STEP_PCB_MODEL::CreatePCB( SHAPE_POLY_SET& aOutline, VECTOR2D aOrigin, bool aPushBoardBody )
  1422. {
  1423. if( m_hasPCB )
  1424. {
  1425. if( !isBoardOutlineValid() )
  1426. return false;
  1427. return true;
  1428. }
  1429. Handle( XCAFDoc_VisMaterialTool ) visMatTool =
  1430. XCAFDoc_DocumentTool::VisMaterialTool( m_doc->Main() );
  1431. m_hasPCB = true; // whether or not operations fail we note that CreatePCB has been invoked
  1432. // Support for more than one main outline (more than one board)
  1433. ReportMessage( wxString::Format( wxT( "Build board outlines (%d outlines) with %d points.\n" ),
  1434. aOutline.OutlineCount(), aOutline.FullPointCount() ) );
  1435. double boardThickness;
  1436. double boardZPos;
  1437. getBoardBodyZPlacement( boardZPos, boardThickness );
  1438. #if 1
  1439. // This code should work, and it is working most of time
  1440. // However there are issues if the main outline is a circle with holes:
  1441. // holes from vias and pads are not working
  1442. // see bug https://gitlab.com/kicad/code/kicad/-/issues/17446
  1443. // (Holes are missing from STEP export with circular PCB outline)
  1444. // Hard to say if the bug is in our code or in OCC 7.7
  1445. if( !MakeShapes( m_board_outlines, aOutline, false, boardThickness, boardZPos, aOrigin ) )
  1446. {
  1447. // Error
  1448. ReportMessage( wxString::Format(
  1449. wxT( "OCC error creating main outline.\n" ) ) );
  1450. }
  1451. #else
  1452. // Workaround for bug #17446 Holes are missing from STEP export with circular PCB outline
  1453. for( const SHAPE_POLY_SET::POLYGON& polygon : aOutline.CPolygons() )
  1454. {
  1455. for( size_t contId = 0; contId < polygon.size(); contId++ )
  1456. {
  1457. const SHAPE_LINE_CHAIN& contour = polygon[contId];
  1458. SHAPE_POLY_SET polyset;
  1459. polyset.Append( contour );
  1460. if( contId == 0 ) // main Outline
  1461. {
  1462. if( !MakeShapes( m_board_outlines, polyset, false, boardThickness, boardZPos,
  1463. aOrigin ) )
  1464. {
  1465. ReportMessage( wxT( "OCC error creating main outline.\n" ) );
  1466. }
  1467. }
  1468. else // Hole inside the main outline
  1469. {
  1470. if( !MakeShapes( m_boardCutouts, polyset, false, boardThickness, boardZPos,
  1471. aOrigin ) )
  1472. {
  1473. ReportMessage( wxT( "OCC error creating hole in main outline.\n" ) );
  1474. }
  1475. }
  1476. }
  1477. }
  1478. #endif
  1479. // Even if we've disabled board body export, we still need the shapes for bounding box calculations.
  1480. Bnd_Box brdBndBox;
  1481. for( const TopoDS_Shape& brdShape : m_board_outlines )
  1482. BRepBndLib::Add( brdShape, brdBndBox );
  1483. // subtract cutouts (if any)
  1484. ReportMessage( wxString::Format( wxT( "Build board cutouts and holes (%d hole(s)).\n" ),
  1485. (int) ( m_boardCutouts.size() + m_copperCutouts.size() ) ) );
  1486. auto buildBSB = [&brdBndBox]( std::vector<TopoDS_Shape>& input, Bnd_BoundSortBox& bsbHoles )
  1487. {
  1488. // We need to encompass every location we'll need to test in the global bbox,
  1489. // otherwise Bnd_BoundSortBox doesn't work near the boundaries.
  1490. Bnd_Box brdWithHolesBndBox = brdBndBox;
  1491. Handle( Bnd_HArray1OfBox ) holeBoxSet = new Bnd_HArray1OfBox( 0, input.size() - 1 );
  1492. for( size_t i = 0; i < input.size(); i++ )
  1493. {
  1494. Bnd_Box bbox;
  1495. BRepBndLib::Add( input[i], bbox );
  1496. brdWithHolesBndBox.Add( bbox );
  1497. ( *holeBoxSet )[i] = bbox;
  1498. }
  1499. bsbHoles.Initialize( brdWithHolesBndBox, holeBoxSet );
  1500. };
  1501. auto subtractShapes = []( const wxString& aWhat, std::vector<TopoDS_Shape>& aShapesList,
  1502. std::vector<TopoDS_Shape>& aHolesList, Bnd_BoundSortBox& aBSBHoles )
  1503. {
  1504. // Remove holes for each item (board body or bodies, one can have more than one board)
  1505. int cnt = 0;
  1506. for( TopoDS_Shape& shape : aShapesList )
  1507. {
  1508. Bnd_Box shapeBbox;
  1509. BRepBndLib::Add( shape, shapeBbox );
  1510. const TColStd_ListOfInteger& indices = aBSBHoles.Compare( shapeBbox );
  1511. TopTools_ListOfShape holelist;
  1512. for( const Standard_Integer& index : indices )
  1513. holelist.Append( aHolesList[index] );
  1514. if( cnt == 0 )
  1515. ReportMessage( wxString::Format( _( "Build holes for %s\n" ), aWhat ) );
  1516. cnt++;
  1517. if( cnt % 10 == 0 )
  1518. ReportMessage( wxString::Format( _( "Cutting %d/%d %s\n" ), cnt,
  1519. (int) aShapesList.size(), aWhat ) );
  1520. if( holelist.IsEmpty() )
  1521. continue;
  1522. TopTools_ListOfShape cutArgs;
  1523. cutArgs.Append( shape );
  1524. BRepAlgoAPI_Cut cut;
  1525. cut.SetRunParallel( true );
  1526. cut.SetToFillHistory( false );
  1527. cut.SetArguments( cutArgs );
  1528. cut.SetTools( holelist );
  1529. cut.Build();
  1530. if( cut.HasErrors() || cut.HasWarnings() )
  1531. {
  1532. ReportMessage( wxString::Format(
  1533. _( "\n** Got problems while cutting %s number %d **\n" ), aWhat, cnt ) );
  1534. shapeBbox.Dump();
  1535. if( cut.HasErrors() )
  1536. {
  1537. ReportMessage( _( "Errors:\n" ) );
  1538. cut.DumpErrors( std::cout );
  1539. }
  1540. if( cut.HasWarnings() )
  1541. {
  1542. ReportMessage( _( "Warnings:\n" ) );
  1543. cut.DumpWarnings( std::cout );
  1544. }
  1545. std::cout << "\n";
  1546. }
  1547. shape = cut.Shape();
  1548. }
  1549. };
  1550. if( m_boardCutouts.size() )
  1551. {
  1552. Bnd_BoundSortBox bsbHoles;
  1553. buildBSB( m_boardCutouts, bsbHoles );
  1554. subtractShapes( _( "shapes" ), m_board_outlines, m_boardCutouts, bsbHoles );
  1555. }
  1556. if( m_copperCutouts.size() )
  1557. {
  1558. Bnd_BoundSortBox bsbHoles;
  1559. buildBSB( m_copperCutouts, bsbHoles );
  1560. subtractShapes( _( "pads" ), m_board_copper_pads, m_copperCutouts, bsbHoles );
  1561. subtractShapes( _( "vias" ), m_board_copper_vias, m_copperCutouts, bsbHoles );
  1562. }
  1563. if( m_fuseShapes )
  1564. {
  1565. ReportMessage( wxT( "Fusing shapes\n" ) );
  1566. TopTools_ListOfShape shapesToFuse;
  1567. for( TopoDS_Shape& shape : m_board_copper )
  1568. shapesToFuse.Append( shape );
  1569. for( TopoDS_Shape& shape : m_board_copper_pads )
  1570. shapesToFuse.Append( shape );
  1571. for( TopoDS_Shape& shape : m_board_copper_vias )
  1572. shapesToFuse.Append( shape );
  1573. TopoDS_Shape fusedShape = fuseShapesOrCompound( shapesToFuse );
  1574. if( !fusedShape.IsNull() )
  1575. {
  1576. m_board_copper_fused.emplace_back( fusedShape );
  1577. m_board_copper.clear();
  1578. m_board_copper_pads.clear();
  1579. m_board_copper_vias.clear();
  1580. }
  1581. }
  1582. // push the board to the data structure
  1583. ReportMessage( wxT( "\nGenerate board full shape.\n" ) );
  1584. // AddComponent adds a label that has a reference (not a parent/child relation) to the real
  1585. // label. We need to extract that real label to name it for the STEP output cleanly
  1586. // Why are we trying to name the bare board? Because CAD tools like SolidWorks do fun things
  1587. // like "deduplicate" imported STEPs by swapping STEP assembly components with already
  1588. // identically named assemblies. So we want to avoid having the PCB be generally defaulted
  1589. // to "Component" or "Assembly".
  1590. auto pushToAssembly = [&]( std::vector<TopoDS_Shape>& aShapesList, Quantity_ColorRGBA aColor,
  1591. const TDF_Label& aVisMatLabel, const wxString& aShapeName,
  1592. bool aCompound )
  1593. {
  1594. if( aShapesList.empty() )
  1595. return;
  1596. std::vector<TopoDS_Shape> newList;
  1597. if( aCompound )
  1598. newList.emplace_back( makeCompound( aShapesList ) );
  1599. else
  1600. newList = aShapesList;
  1601. int i = 1;
  1602. for( TopoDS_Shape& shape : newList )
  1603. {
  1604. Handle( TDataStd_TreeNode ) node;
  1605. // Dont expand the component or else coloring it gets hard
  1606. TDF_Label lbl = m_assy->AddComponent( m_assy_label, shape, false );
  1607. m_pcb_labels.push_back( lbl );
  1608. if( m_pcb_labels.back().IsNull() )
  1609. return;
  1610. lbl.FindAttribute( XCAFDoc::ShapeRefGUID(), node );
  1611. TDF_Label shpLbl = node->Father()->Label();
  1612. if( !shpLbl.IsNull() )
  1613. {
  1614. if( visMatTool && !aVisMatLabel.IsNull() )
  1615. visMatTool->SetShapeMaterial( shpLbl, aVisMatLabel );
  1616. wxString shapeName;
  1617. if( newList.size() > 1 )
  1618. {
  1619. shapeName = wxString::Format( wxT( "%s_%s_%d" ), m_pcbName, aShapeName, i );
  1620. }
  1621. else
  1622. {
  1623. shapeName = wxString::Format( wxT( "%s_%s" ), m_pcbName, aShapeName );
  1624. }
  1625. TCollection_ExtendedString partname( shapeName.ToUTF8().data() );
  1626. TDataStd_Name::Set( shpLbl, partname );
  1627. }
  1628. i++;
  1629. }
  1630. };
  1631. auto makeMaterial = [&]( const TCollection_AsciiString& aName,
  1632. const Quantity_ColorRGBA& aBaseColor, double aMetallic,
  1633. double aRoughness ) -> TDF_Label
  1634. {
  1635. Handle( XCAFDoc_VisMaterial ) vismat = new XCAFDoc_VisMaterial;
  1636. XCAFDoc_VisMaterialPBR pbr;
  1637. pbr.BaseColor = aBaseColor;
  1638. pbr.Metallic = aMetallic;
  1639. pbr.Roughness = aRoughness;
  1640. vismat->SetPbrMaterial( pbr );
  1641. return visMatTool->AddMaterial( vismat, aName );
  1642. };
  1643. // Init colors for the board items
  1644. Quantity_ColorRGBA copper_color( m_copperColor[0], m_copperColor[1], m_copperColor[2], 1.0 );
  1645. Quantity_ColorRGBA pad_color( m_padColor[0], m_padColor[1], m_padColor[2], 1.0 );
  1646. Quantity_ColorRGBA board_color( 0.3f, 0.3f, 0.3f, 1.0f );
  1647. Quantity_ColorRGBA silk_color( 1.0f, 1.0f, 1.0f, 0.9f );
  1648. Quantity_ColorRGBA mask_color( 0.08f, 0.2f, 0.14f, 0.83f );
  1649. // Get colors from stackup
  1650. for( const BOARD_STACKUP_ITEM* item : m_stackup.GetList() )
  1651. {
  1652. COLOR4D col;
  1653. if( !colorFromStackup( item->GetType(), item->GetColor(), col ) )
  1654. continue;
  1655. if( item->GetBrdLayerId() == F_Mask || item->GetBrdLayerId() == B_Mask )
  1656. {
  1657. col.Darken( 0.2 );
  1658. mask_color.SetValues( col.r, col.g, col.b, col.a );
  1659. }
  1660. if( item->GetBrdLayerId() == F_SilkS || item->GetBrdLayerId() == B_SilkS )
  1661. silk_color.SetValues( col.r, col.g, col.b, col.a );
  1662. if( item->GetType() == BS_ITEM_TYPE_DIELECTRIC && item->GetTypeName() == KEY_CORE )
  1663. board_color.SetValues( col.r, col.g, col.b, col.a );
  1664. }
  1665. if( !m_enabledLayers.Contains( F_Mask ) && !m_enabledLayers.Contains( B_Mask ) )
  1666. {
  1667. board_color = mask_color;
  1668. board_color.SetAlpha( 1.0 );
  1669. }
  1670. TDF_Label mask_mat = makeMaterial( "soldermask", mask_color, 0.0, 0.6 );
  1671. TDF_Label silk_mat = makeMaterial( "silkscreen", silk_color, 0.0, 0.9 );
  1672. TDF_Label copper_mat = makeMaterial( "copper", copper_color, 1.0, 0.4 );
  1673. TDF_Label pad_mat = makeMaterial( "pad", pad_color, 1.0, 0.4 );
  1674. TDF_Label board_mat = makeMaterial( "board", board_color, 0.0, 0.8 );
  1675. pushToAssembly( m_board_copper, copper_color, copper_mat, "copper", true );
  1676. pushToAssembly( m_board_copper_pads, pad_color, pad_mat, "pad", true );
  1677. pushToAssembly( m_board_copper_vias, copper_color, copper_mat, "via", true );
  1678. pushToAssembly( m_board_copper_fused, copper_color, copper_mat, "copper", true );
  1679. pushToAssembly( m_board_silkscreen, silk_color, silk_mat, "silkscreen", true );
  1680. pushToAssembly( m_board_soldermask, mask_color, mask_mat, "soldermask", true );
  1681. if( aPushBoardBody )
  1682. pushToAssembly( m_board_outlines, board_color, board_mat, "PCB", false );
  1683. #if( defined OCC_VERSION_HEX ) && ( OCC_VERSION_HEX > 0x070101 )
  1684. m_assy->UpdateAssemblies();
  1685. #endif
  1686. return true;
  1687. }
  1688. #ifdef SUPPORTS_IGES
  1689. // write the assembly model in IGES format
  1690. bool STEP_PCB_MODEL::WriteIGES( const wxString& aFileName )
  1691. {
  1692. if( !isBoardOutlineValid() )
  1693. {
  1694. ReportMessage( wxString::Format( wxT( "No valid PCB assembly; cannot create output file "
  1695. "'%s'.\n" ),
  1696. aFileName ) );
  1697. return false;
  1698. }
  1699. m_outFmt = OUTPUT_FORMAT::FMT_OUT_IGES;
  1700. wxFileName fn( aFileName );
  1701. IGESControl_Controller::Init();
  1702. IGESCAFControl_Writer writer;
  1703. writer.SetColorMode( Standard_True );
  1704. writer.SetNameMode( Standard_True );
  1705. IGESData_GlobalSection header = writer.Model()->GlobalSection();
  1706. header.SetFileName( new TCollection_HAsciiString( fn.GetFullName().ToAscii() ) );
  1707. header.SetSendName( new TCollection_HAsciiString( "KiCad electronic assembly" ) );
  1708. header.SetAuthorName(
  1709. new TCollection_HAsciiString( Interface_Static::CVal( "write.iges.header.author" ) ) );
  1710. header.SetCompanyName(
  1711. new TCollection_HAsciiString( Interface_Static::CVal( "write.iges.header.company" ) ) );
  1712. writer.Model()->SetGlobalSection( header );
  1713. if( Standard_False == writer.Perform( m_doc, aFileName.c_str() ) )
  1714. return false;
  1715. return true;
  1716. }
  1717. #endif
  1718. bool STEP_PCB_MODEL::WriteSTEP( const wxString& aFileName, bool aOptimize )
  1719. {
  1720. if( !isBoardOutlineValid() )
  1721. {
  1722. ReportMessage( wxString::Format( wxT( "No valid PCB assembly; cannot create output file "
  1723. "'%s'.\n" ),
  1724. aFileName ) );
  1725. return false;
  1726. }
  1727. m_outFmt = OUTPUT_FORMAT::FMT_OUT_STEP;
  1728. wxFileName fn( aFileName );
  1729. STEPCAFControl_Writer writer;
  1730. writer.SetColorMode( Standard_True );
  1731. writer.SetNameMode( Standard_True );
  1732. // This must be set before we "transfer" the document.
  1733. // Should default to kicad_pcb.general.title_block.title,
  1734. // but in the meantime, defaulting to the basename of the output
  1735. // target is still better than "open cascade step translter v..."
  1736. // UTF8 should be ok from ISO 10303-21:2016, but... older stuff? use boring ascii
  1737. if( !Interface_Static::SetCVal( "write.step.product.name", fn.GetName().ToAscii() ) )
  1738. ReportMessage( wxT( "Failed to set step product name, but will attempt to continue." ) );
  1739. // Setting write.surfacecurve.mode to 0 reduces file size and write/read times.
  1740. // But there are reports that this mode might be less compatible in some cases.
  1741. if( !Interface_Static::SetIVal( "write.surfacecurve.mode", aOptimize ? 0 : 1 ) )
  1742. ReportMessage( wxT( "Failed to set surface curve mode, but will attempt to continue." ) );
  1743. if( Standard_False == writer.Transfer( m_doc, STEPControl_AsIs ) )
  1744. return false;
  1745. APIHeaderSection_MakeHeader hdr( writer.ChangeWriter().Model() );
  1746. // Note: use only Ascii7 chars, non Ascii7 chars (therefore UFT8 chars)
  1747. // are creating issues in the step file
  1748. hdr.SetName( new TCollection_HAsciiString( fn.GetFullName().ToAscii() ) );
  1749. // TODO: how to control and ensure consistency with IGES?
  1750. hdr.SetAuthorValue( 1, new TCollection_HAsciiString( "Pcbnew" ) );
  1751. hdr.SetOrganizationValue( 1, new TCollection_HAsciiString( "Kicad" ) );
  1752. hdr.SetOriginatingSystem( new TCollection_HAsciiString( "KiCad to STEP converter" ) );
  1753. hdr.SetDescriptionValue( 1, new TCollection_HAsciiString( "KiCad electronic assembly" ) );
  1754. bool success = true;
  1755. // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
  1756. wxString currCWD = wxGetCwd();
  1757. wxString workCWD = fn.GetPath();
  1758. if( !workCWD.IsEmpty() )
  1759. wxSetWorkingDirectory( workCWD );
  1760. char tmpfname[] = "$tempfile$.step";
  1761. if( Standard_False == writer.Write( tmpfname ) )
  1762. success = false;
  1763. if( success )
  1764. {
  1765. // Preserve the permissions of the current file
  1766. KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpfname );
  1767. if( !wxRenameFile( tmpfname, fn.GetFullName(), true ) )
  1768. {
  1769. ReportMessage( wxString::Format( wxT( "Cannot rename temporary file '%s' to '%s'.\n" ),
  1770. tmpfname,
  1771. fn.GetFullName() ) );
  1772. success = false;
  1773. }
  1774. }
  1775. wxSetWorkingDirectory( currCWD );
  1776. return success;
  1777. }
  1778. bool STEP_PCB_MODEL::WriteBREP( const wxString& aFileName )
  1779. {
  1780. if( !isBoardOutlineValid() )
  1781. {
  1782. ReportMessage( wxString::Format( wxT( "No valid PCB assembly; cannot create output file "
  1783. "'%s'.\n" ),
  1784. aFileName ) );
  1785. return false;
  1786. }
  1787. m_outFmt = OUTPUT_FORMAT::FMT_OUT_BREP;
  1788. // s_assy = shape tool for the source
  1789. Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
  1790. // retrieve assembly as a single shape
  1791. TopoDS_Shape shape = getOneShape( s_assy );
  1792. wxFileName fn( aFileName );
  1793. wxFFileOutputStream ffStream( fn.GetFullPath() );
  1794. wxStdOutputStream stdStream( ffStream );
  1795. #if OCC_VERSION_HEX >= 0x070600
  1796. BRepTools::Write( shape, stdStream, false, false, TopTools_FormatVersion_VERSION_1 );
  1797. #else
  1798. BRepTools::Write( shape, stdStream );
  1799. #endif
  1800. return true;
  1801. }
  1802. bool STEP_PCB_MODEL::WriteXAO( const wxString& aFileName )
  1803. {
  1804. wxFileName fn( aFileName );
  1805. wxFFileOutputStream ffStream( fn.GetFullPath() );
  1806. wxStdOutputStream file( ffStream );
  1807. if( !ffStream.IsOk() )
  1808. {
  1809. ReportMessage( wxString::Format( "Could not open file '%s'", fn.GetFullPath() ) );
  1810. return false;
  1811. }
  1812. m_outFmt = OUTPUT_FORMAT::FMT_OUT_XAO;
  1813. // s_assy = shape tool for the source
  1814. Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
  1815. // retrieve assembly as a single shape
  1816. const TopoDS_Shape shape = getOneShape( s_assy );
  1817. std::map<wxString, std::vector<int>> groups[4];
  1818. TopExp_Explorer exp;
  1819. int faceIndex = 0;
  1820. for( exp.Init( shape, TopAbs_FACE ); exp.More(); exp.Next() )
  1821. {
  1822. TopoDS_Shape subShape = exp.Current();
  1823. Bnd_Box bbox;
  1824. BRepBndLib::Add( subShape, bbox );
  1825. for( const auto& [padKey, pair] : m_pad_points )
  1826. {
  1827. const auto& [point, padTestShape] = pair;
  1828. if( bbox.IsOut( point ) )
  1829. continue;
  1830. BRepAdaptor_Surface surface( TopoDS::Face( subShape ) );
  1831. if( surface.GetType() != GeomAbs_Plane )
  1832. continue;
  1833. BRepExtrema_DistShapeShape dist( padTestShape, subShape );
  1834. dist.Perform();
  1835. if( !dist.IsDone() )
  1836. continue;
  1837. if( dist.Value() < Precision::Approximation() )
  1838. {
  1839. // Push as a face group
  1840. groups[2][padKey].push_back( faceIndex );
  1841. }
  1842. }
  1843. faceIndex++;
  1844. }
  1845. // Based on Gmsh code
  1846. file << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << std::endl;
  1847. file << "<XAO version=\"1.0\" author=\"KiCad\">" << std::endl;
  1848. file << " <geometry name=\"" << fn.GetName() << "\">" << std::endl;
  1849. file << " <shape format=\"BREP\"><![CDATA[";
  1850. #if OCC_VERSION_HEX < 0x070600
  1851. BRepTools::Write( shape, file );
  1852. #else
  1853. BRepTools::Write( shape, file, Standard_True, Standard_True, TopTools_FormatVersion_VERSION_1 );
  1854. #endif
  1855. file << "]]></shape>" << std::endl;
  1856. file << " <topology>" << std::endl;
  1857. TopTools_IndexedMapOfShape mainMap;
  1858. TopExp::MapShapes( shape, mainMap );
  1859. std::set<int> topo[4];
  1860. static const TopAbs_ShapeEnum c_dimShapeTypes[] = { TopAbs_VERTEX, TopAbs_EDGE, TopAbs_FACE,
  1861. TopAbs_SOLID };
  1862. static const std::string c_dimLabel[] = { "vertex", "edge", "face", "solid" };
  1863. static const std::string c_dimLabels[] = { "vertices", "edges", "faces", "solids" };
  1864. for( int dim = 0; dim < 4; dim++ )
  1865. {
  1866. for( exp.Init( shape, c_dimShapeTypes[dim] ); exp.More(); exp.Next() )
  1867. {
  1868. TopoDS_Shape subShape = exp.Current();
  1869. int idx = mainMap.FindIndex( subShape );
  1870. if( idx && !topo[dim].count( idx ) )
  1871. topo[dim].insert( idx );
  1872. }
  1873. }
  1874. for( int dim = 0; dim <= 3; dim++ )
  1875. {
  1876. std::string labels = c_dimLabels[dim];
  1877. std::string label = c_dimLabel[dim];
  1878. file << " <" << labels << " count=\"" << topo[dim].size() << "\">" << std::endl;
  1879. int index = 0;
  1880. for( auto p : topo[dim] )
  1881. {
  1882. std::string name( "" );
  1883. file << " <" << label << " index=\"" << index << "\" "
  1884. << "name=\"" << name << "\" "
  1885. << "reference=\"" << p << "\"/>" << std::endl;
  1886. index++;
  1887. }
  1888. file << " </" << labels << ">" << std::endl;
  1889. }
  1890. file << " </topology>" << std::endl;
  1891. file << " </geometry>" << std::endl;
  1892. file << " <groups count=\""
  1893. << groups[0].size() + groups[1].size() + groups[2].size() + groups[3].size() << "\">"
  1894. << std::endl;
  1895. for( int dim = 0; dim <= 3; dim++ )
  1896. {
  1897. std::string label = c_dimLabel[dim];
  1898. for( auto g : groups[dim] )
  1899. {
  1900. //std::string name = model->getPhysicalName( dim, g.first );
  1901. wxString name = g.first;
  1902. if( name.empty() )
  1903. { // create same unique name as for MED export
  1904. std::ostringstream gs;
  1905. gs << "G_" << dim << "D_" << g.first;
  1906. name = gs.str();
  1907. }
  1908. file << " <group name=\"" << name << "\" dimension=\"" << label;
  1909. //#if 1
  1910. // // Gmsh XAO extension: also save the physical tag, so that XAO can be used
  1911. // // to serialize OCC geometries, ready to be used by GetDP, GmshFEM & co
  1912. // file << "\" tag=\"" << g.first;
  1913. //#endif
  1914. file << "\" count=\"" << g.second.size() << "\">" << std::endl;
  1915. for( auto index : g.second )
  1916. {
  1917. file << " <element index=\"" << index << "\"/>" << std::endl;
  1918. }
  1919. file << " </group>" << std::endl;
  1920. }
  1921. }
  1922. file << " </groups>" << std::endl;
  1923. file << " <fields count=\"0\"/>" << std::endl;
  1924. file << "</XAO>" << std::endl;
  1925. return true;
  1926. }
  1927. bool STEP_PCB_MODEL::getModelLabel( const std::string& aFileNameUTF8, VECTOR3D aScale, TDF_Label& aLabel,
  1928. bool aSubstituteModels, wxString* aErrorMessage )
  1929. {
  1930. std::string model_key = aFileNameUTF8 + "_" + std::to_string( aScale.x )
  1931. + "_" + std::to_string( aScale.y ) + "_" + std::to_string( aScale.z );
  1932. MODEL_MAP::const_iterator mm = m_models.find( model_key );
  1933. if( mm != m_models.end() )
  1934. {
  1935. aLabel = mm->second;
  1936. return true;
  1937. }
  1938. aLabel.Nullify();
  1939. Handle( TDocStd_Document ) doc;
  1940. m_app->NewDocument( "MDTV-XCAF", doc );
  1941. wxString fileName( wxString::FromUTF8( aFileNameUTF8.c_str() ) );
  1942. MODEL3D_FORMAT_TYPE modelFmt = fileType( aFileNameUTF8.c_str() );
  1943. switch( modelFmt )
  1944. {
  1945. case FMT_IGES:
  1946. if( !readIGES( doc, aFileNameUTF8.c_str() ) )
  1947. {
  1948. ReportMessage( wxString::Format( wxT( "readIGES() failed on filename '%s'.\n" ),
  1949. fileName ) );
  1950. return false;
  1951. }
  1952. break;
  1953. case FMT_STEP:
  1954. if( !readSTEP( doc, aFileNameUTF8.c_str() ) )
  1955. {
  1956. ReportMessage( wxString::Format( wxT( "readSTEP() failed on filename '%s'.\n" ),
  1957. fileName ) );
  1958. return false;
  1959. }
  1960. break;
  1961. case FMT_STEPZ:
  1962. {
  1963. // To export a compressed step file (.stpz or .stp.gz file), the best way is to
  1964. // decaompress it in a temporaty file and load this temporary file
  1965. wxFFileInputStream ifile( fileName );
  1966. wxFileName outFile( fileName );
  1967. outFile.SetPath( wxStandardPaths::Get().GetTempDir() );
  1968. outFile.SetExt( wxT( "step" ) );
  1969. wxFileOffset size = ifile.GetLength();
  1970. if( size == wxInvalidOffset )
  1971. {
  1972. ReportMessage( wxString::Format( wxT( "getModelLabel() failed on filename '%s'.\n" ),
  1973. fileName ) );
  1974. return false;
  1975. }
  1976. {
  1977. bool success = false;
  1978. wxFFileOutputStream ofile( outFile.GetFullPath() );
  1979. if( !ofile.IsOk() )
  1980. return false;
  1981. char* buffer = new char[size];
  1982. ifile.Read( buffer, size );
  1983. std::string expanded;
  1984. try
  1985. {
  1986. expanded = gzip::decompress( buffer, size );
  1987. success = true;
  1988. }
  1989. catch( ... )
  1990. {
  1991. ReportMessage( wxString::Format( wxT( "failed to decompress '%s'.\n" ),
  1992. fileName ) );
  1993. }
  1994. if( expanded.empty() )
  1995. {
  1996. ifile.Reset();
  1997. ifile.SeekI( 0 );
  1998. wxZipInputStream izipfile( ifile );
  1999. std::unique_ptr<wxZipEntry> zip_file( izipfile.GetNextEntry() );
  2000. if( zip_file && !zip_file->IsDir() && izipfile.CanRead() )
  2001. {
  2002. izipfile.Read( ofile );
  2003. success = true;
  2004. }
  2005. }
  2006. else
  2007. {
  2008. ofile.Write( expanded.data(), expanded.size() );
  2009. }
  2010. delete[] buffer;
  2011. ofile.Close();
  2012. if( success )
  2013. {
  2014. std::string altFileNameUTF8 = TO_UTF8( outFile.GetFullPath() );
  2015. success =
  2016. getModelLabel( altFileNameUTF8, VECTOR3D( 1.0, 1.0, 1.0 ), aLabel, false );
  2017. }
  2018. return success;
  2019. }
  2020. break;
  2021. }
  2022. case FMT_WRL:
  2023. case FMT_WRZ:
  2024. /* WRL files are preferred for internal rendering, due to superior material properties, etc.
  2025. * However they are not suitable for MCAD export.
  2026. *
  2027. * If a .wrl file is specified, attempt to locate a replacement file for it.
  2028. *
  2029. * If a valid replacement file is found, the label for THAT file will be associated with
  2030. * the .wrl file
  2031. */
  2032. if( aSubstituteModels )
  2033. {
  2034. wxFileName wrlName( fileName );
  2035. wxString basePath = wrlName.GetPath();
  2036. wxString baseName = wrlName.GetName();
  2037. // List of alternate files to look for
  2038. // Given in order of preference
  2039. // (Break if match is found)
  2040. wxArrayString alts;
  2041. // Step files
  2042. alts.Add( wxT( "stp" ) );
  2043. alts.Add( wxT( "step" ) );
  2044. alts.Add( wxT( "STP" ) );
  2045. alts.Add( wxT( "STEP" ) );
  2046. alts.Add( wxT( "Stp" ) );
  2047. alts.Add( wxT( "Step" ) );
  2048. alts.Add( wxT( "stpz" ) );
  2049. alts.Add( wxT( "stpZ" ) );
  2050. alts.Add( wxT( "STPZ" ) );
  2051. alts.Add( wxT( "step.gz" ) );
  2052. alts.Add( wxT( "stp.gz" ) );
  2053. // IGES files
  2054. alts.Add( wxT( "iges" ) );
  2055. alts.Add( wxT( "IGES" ) );
  2056. alts.Add( wxT( "igs" ) );
  2057. alts.Add( wxT( "IGS" ) );
  2058. //TODO - Other alternative formats?
  2059. for( const auto& alt : alts )
  2060. {
  2061. wxFileName altFile( basePath, baseName + wxT( "." ) + alt );
  2062. if( altFile.IsOk() && altFile.FileExists() )
  2063. {
  2064. std::string altFileNameUTF8 = TO_UTF8( altFile.GetFullPath() );
  2065. // When substituting a STEP/IGS file for VRML, do not apply the VRML scaling
  2066. // to the new STEP model. This process of auto-substitution is janky as all
  2067. // heck so let's not mix up un-displayed scale factors with potentially
  2068. // mis-matched files. And hope that the user doesn't have multiples files
  2069. // named "model.wrl" and "model.stp" referring to different parts.
  2070. // TODO: Fix model handling in v7. Default models should only be STP.
  2071. // Have option to override this in DISPLAY.
  2072. if( getModelLabel( altFileNameUTF8, VECTOR3D( 1.0, 1.0, 1.0 ), aLabel, false ) )
  2073. {
  2074. return true;
  2075. }
  2076. }
  2077. }
  2078. // VRML models only work when exporting to glTF
  2079. // Also OCCT < 7.9.0 fail to load most VRML 2.0 models because of Switch nodes
  2080. if( m_outFmt == OUTPUT_FORMAT::FMT_OUT_GLTF )
  2081. {
  2082. if( readVRML( doc, aFileNameUTF8.c_str() ) )
  2083. {
  2084. Handle( XCAFDoc_ShapeTool ) shapeTool =
  2085. XCAFDoc_DocumentTool::ShapeTool( doc->Main() );
  2086. prefixNames( shapeTool->Label(),
  2087. TCollection_ExtendedString( baseName.c_str().AsChar() ) );
  2088. }
  2089. else
  2090. {
  2091. ReportMessage( wxString::Format( wxT( "readVRML() failed on filename '%s'.\n" ),
  2092. fileName ) );
  2093. return false;
  2094. }
  2095. }
  2096. }
  2097. else // Substitution is not allowed
  2098. {
  2099. if( aErrorMessage )
  2100. aErrorMessage->Printf( wxT( "Cannot load any VRML model for this export.\n" ) );
  2101. return false;
  2102. }
  2103. break;
  2104. // TODO: implement IDF and EMN converters
  2105. default:
  2106. ReportMessage( wxString::Format( wxT( "Cannot identify actual file type for '%s'.\n" ),
  2107. fileName ) );
  2108. return false;
  2109. }
  2110. aLabel = transferModel( doc, m_doc, aScale );
  2111. if( aLabel.IsNull() )
  2112. {
  2113. ReportMessage( wxString::Format( wxT( "Could not transfer model data from file '%s'.\n" ),
  2114. fileName ) );
  2115. return false;
  2116. }
  2117. // attach the PART NAME ( base filename: note that in principle
  2118. // different models may have the same base filename )
  2119. wxFileName afile( fileName );
  2120. std::string pname( afile.GetName().ToUTF8() );
  2121. TCollection_ExtendedString partname( pname.c_str() );
  2122. TDataStd_Name::Set( aLabel, partname );
  2123. m_models.insert( MODEL_DATUM( model_key, aLabel ) );
  2124. ++m_components;
  2125. return true;
  2126. }
  2127. bool STEP_PCB_MODEL::getModelLocation( bool aBottom, VECTOR2D aPosition, double aRotation, VECTOR3D aOffset, VECTOR3D aOrientation,
  2128. TopLoc_Location& aLocation )
  2129. {
  2130. // Order of operations:
  2131. // a. aOrientation is applied -Z*-Y*-X
  2132. // b. aOffset is applied
  2133. // Top ? add thickness to the Z offset
  2134. // c. Bottom ? Rotate on X axis (in contrast to most ECAD which mirror on Y),
  2135. // then rotate on +Z
  2136. // Top ? rotate on -Z
  2137. // d. aPosition is applied
  2138. //
  2139. // Note: Y axis is inverted in KiCad
  2140. gp_Trsf lPos;
  2141. lPos.SetTranslation( gp_Vec( aPosition.x, -aPosition.y, 0.0 ) );
  2142. // Offset board thickness
  2143. aOffset.z += BOARD_OFFSET;
  2144. double boardThickness;
  2145. double boardZPos;
  2146. getBoardBodyZPlacement( boardZPos, boardThickness );
  2147. double top = std::max( boardZPos, boardZPos + boardThickness );
  2148. double bottom = std::min( boardZPos, boardZPos + boardThickness );
  2149. // 3D step models are placed on the top of copper layers.
  2150. // This is true for SMD shapes, and perhaps not always true for TH shapes,
  2151. // but we use this Z position for any 3D shape.
  2152. double f_pos, f_thickness;
  2153. getLayerZPlacement( F_Cu, f_pos, f_thickness );
  2154. top += f_thickness;
  2155. getLayerZPlacement( B_Cu, f_pos, f_thickness );
  2156. bottom += f_thickness; // f_thickness is < 0 for B_Cu layer
  2157. gp_Trsf lRot;
  2158. if( aBottom )
  2159. {
  2160. aOffset.z -= bottom;
  2161. lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), aRotation );
  2162. lPos.Multiply( lRot );
  2163. lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 1.0, 0.0, 0.0 ) ), M_PI );
  2164. lPos.Multiply( lRot );
  2165. }
  2166. else
  2167. {
  2168. aOffset.z += top;
  2169. lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), aRotation );
  2170. lPos.Multiply( lRot );
  2171. }
  2172. gp_Trsf lOff;
  2173. lOff.SetTranslation( gp_Vec( aOffset.x, aOffset.y, aOffset.z ) );
  2174. lPos.Multiply( lOff );
  2175. gp_Trsf lOrient;
  2176. lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ),
  2177. -aOrientation.z );
  2178. lPos.Multiply( lOrient );
  2179. lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 1.0, 0.0 ) ),
  2180. -aOrientation.y );
  2181. lPos.Multiply( lOrient );
  2182. lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 1.0, 0.0, 0.0 ) ),
  2183. -aOrientation.x );
  2184. lPos.Multiply( lOrient );
  2185. aLocation = TopLoc_Location( lPos );
  2186. return true;
  2187. }
  2188. bool STEP_PCB_MODEL::readIGES( Handle( TDocStd_Document )& doc, const char* fname )
  2189. {
  2190. IGESControl_Controller::Init();
  2191. IGESCAFControl_Reader reader;
  2192. IFSelect_ReturnStatus stat = reader.ReadFile( fname );
  2193. if( stat != IFSelect_RetDone )
  2194. return false;
  2195. // Enable user-defined shape precision
  2196. if( !Interface_Static::SetIVal( "read.precision.mode", 1 ) )
  2197. return false;
  2198. // Set the shape conversion precision to USER_PREC (default 0.0001 has too many triangles)
  2199. if( !Interface_Static::SetRVal( "read.precision.val", USER_PREC ) )
  2200. return false;
  2201. // set other translation options
  2202. reader.SetColorMode( true ); // use model colors
  2203. reader.SetNameMode( false ); // don't use IGES label names
  2204. reader.SetLayerMode( false ); // ignore LAYER data
  2205. if( !reader.Transfer( doc ) )
  2206. {
  2207. if( doc->CanClose() == CDM_CCS_OK )
  2208. doc->Close();
  2209. return false;
  2210. }
  2211. // are there any shapes to translate?
  2212. if( reader.NbShapes() < 1 )
  2213. {
  2214. if( doc->CanClose() == CDM_CCS_OK )
  2215. doc->Close();
  2216. return false;
  2217. }
  2218. return true;
  2219. }
  2220. bool STEP_PCB_MODEL::readSTEP( Handle( TDocStd_Document )& doc, const char* fname )
  2221. {
  2222. STEPCAFControl_Reader reader;
  2223. IFSelect_ReturnStatus stat = reader.ReadFile( fname );
  2224. if( stat != IFSelect_RetDone )
  2225. return false;
  2226. // Enable user-defined shape precision
  2227. if( !Interface_Static::SetIVal( "read.precision.mode", 1 ) )
  2228. return false;
  2229. // Set the shape conversion precision to USER_PREC (default 0.0001 has too many triangles)
  2230. if( !Interface_Static::SetRVal( "read.precision.val", USER_PREC ) )
  2231. return false;
  2232. // set other translation options
  2233. reader.SetColorMode( true ); // use model colors
  2234. reader.SetNameMode( true ); // use label names
  2235. reader.SetLayerMode( false ); // ignore LAYER data
  2236. if( !reader.Transfer( doc ) )
  2237. {
  2238. if( doc->CanClose() == CDM_CCS_OK )
  2239. doc->Close();
  2240. return false;
  2241. }
  2242. // are there any shapes to translate?
  2243. if( reader.NbRootsForTransfer() < 1 )
  2244. {
  2245. if( doc->CanClose() == CDM_CCS_OK )
  2246. doc->Close();
  2247. return false;
  2248. }
  2249. return true;
  2250. }
  2251. bool STEP_PCB_MODEL::readVRML( Handle( TDocStd_Document ) & doc, const char* fname )
  2252. {
  2253. #if OCC_VERSION_HEX >= 0x070700
  2254. VrmlAPI_CafReader reader;
  2255. RWMesh_CoordinateSystemConverter conv;
  2256. conv.SetInputLengthUnit( 2.54 );
  2257. reader.SetCoordinateSystemConverter( conv );
  2258. reader.SetDocument( doc );
  2259. if( !reader.Perform( TCollection_AsciiString( fname ), Message_ProgressRange() ) )
  2260. return false;
  2261. return true;
  2262. #else
  2263. return false;
  2264. #endif
  2265. }
  2266. TDF_Label STEP_PCB_MODEL::transferModel( Handle( TDocStd_Document ) & source,
  2267. Handle( TDocStd_Document ) & dest, VECTOR3D aScale )
  2268. {
  2269. // transfer data from Source into a top level component of Dest
  2270. // s_assy = shape tool for the source
  2271. Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( source->Main() );
  2272. // retrieve all free shapes within the assembly
  2273. TDF_LabelSequence frshapes;
  2274. s_assy->GetFreeShapes( frshapes );
  2275. // d_assy = shape tool for the destination
  2276. Handle( XCAFDoc_ShapeTool ) d_assy = XCAFDoc_DocumentTool::ShapeTool( dest->Main() );
  2277. // create a new shape within the destination and set the assembly tool to point to it
  2278. TDF_Label d_targetLabel = d_assy->NewShape();
  2279. if( frshapes.Size() == 1 )
  2280. {
  2281. TDocStd_XLinkTool link;
  2282. link.Copy( d_targetLabel, frshapes.First() );
  2283. }
  2284. else
  2285. {
  2286. // Rare case
  2287. for( TDF_Label& s_shapeLabel : frshapes )
  2288. {
  2289. TDF_Label d_component = d_assy->NewShape();
  2290. TDocStd_XLinkTool link;
  2291. link.Copy( d_component, s_shapeLabel );
  2292. d_assy->AddComponent( d_targetLabel, d_component, TopLoc_Location() );
  2293. }
  2294. }
  2295. if( aScale.x != 1.0 || aScale.y != 1.0 || aScale.z != 1.0 )
  2296. rescaleShapes( d_targetLabel, gp_XYZ( aScale.x, aScale.y, aScale.z ) );
  2297. return d_targetLabel;
  2298. }
  2299. bool STEP_PCB_MODEL::WriteGLTF( const wxString& aFileName )
  2300. {
  2301. if( !isBoardOutlineValid() )
  2302. {
  2303. ReportMessage( wxString::Format( wxT( "No valid PCB assembly; cannot create output file "
  2304. "'%s'.\n" ),
  2305. aFileName ) );
  2306. return false;
  2307. }
  2308. m_outFmt = OUTPUT_FORMAT::FMT_OUT_GLTF;
  2309. TDF_LabelSequence freeShapes;
  2310. m_assy->GetFreeShapes( freeShapes );
  2311. ReportMessage( wxT( "Meshing model\n" ) );
  2312. // GLTF is a mesh format, we have to trigger opencascade to mesh the shapes we composited into the asesmbly
  2313. // To mesh models, lets just grab the free shape root and execute on them
  2314. for( Standard_Integer i = 1; i <= freeShapes.Length(); ++i )
  2315. {
  2316. TDF_Label label = freeShapes.Value( i );
  2317. TopoDS_Shape shape;
  2318. m_assy->GetShape( label, shape );
  2319. // These deflection values basically affect the accuracy of the mesh generated, a tighter
  2320. // deflection will result in larger meshes
  2321. // We could make this a tunable parameter, but for now fix it
  2322. const Standard_Real linearDeflection = 0.14;
  2323. const Standard_Real angularDeflection = DEG2RAD( 30.0 );
  2324. BRepMesh_IncrementalMesh mesh( shape, linearDeflection, Standard_False, angularDeflection,
  2325. Standard_True );
  2326. }
  2327. wxFileName fn( aFileName );
  2328. const char* tmpGltfname = "$tempfile$.glb";
  2329. RWGltf_CafWriter cafWriter( tmpGltfname, true );
  2330. cafWriter.SetTransformationFormat( RWGltf_WriterTrsfFormat_Compact );
  2331. cafWriter.ChangeCoordinateSystemConverter().SetInputLengthUnit( 0.001 );
  2332. cafWriter.ChangeCoordinateSystemConverter().SetInputCoordinateSystem(
  2333. RWMesh_CoordinateSystem_Zup );
  2334. #if OCC_VERSION_HEX >= 0x070700
  2335. cafWriter.SetParallel( true );
  2336. #endif
  2337. TColStd_IndexedDataMapOfStringString metadata;
  2338. metadata.Add( TCollection_AsciiString( "pcb_name" ),
  2339. TCollection_ExtendedString( fn.GetName().wc_str() ) );
  2340. metadata.Add( TCollection_AsciiString( "source_pcb_file" ),
  2341. TCollection_ExtendedString( fn.GetFullName().wc_str() ) );
  2342. metadata.Add( TCollection_AsciiString( "generator" ),
  2343. TCollection_AsciiString( wxString::Format( wxS( "KiCad %s" ), GetSemanticVersion() ).ToAscii() ) );
  2344. metadata.Add( TCollection_AsciiString( "generated_at" ),
  2345. TCollection_AsciiString( GetISO8601CurrentDateTime().ToAscii() ) );
  2346. bool success = true;
  2347. // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
  2348. wxString currCWD = wxGetCwd();
  2349. wxString workCWD = fn.GetPath();
  2350. if( !workCWD.IsEmpty() )
  2351. wxSetWorkingDirectory( workCWD );
  2352. success = cafWriter.Perform( m_doc, metadata, Message_ProgressRange() );
  2353. if( success )
  2354. {
  2355. // Preserve the permissions of the current file
  2356. KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpGltfname );
  2357. if( !wxRenameFile( tmpGltfname, fn.GetFullName(), true ) )
  2358. {
  2359. ReportMessage( wxString::Format( wxT( "Cannot rename temporary file '%s' to '%s'.\n" ),
  2360. tmpGltfname, fn.GetFullName() ) );
  2361. success = false;
  2362. }
  2363. }
  2364. wxSetWorkingDirectory( currCWD );
  2365. return success;
  2366. }