KiCad PCB EDA Suite
Loading...
Searching...
No Matches
step_pcb_model.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2022 Mark Roszko <[email protected]>
5 * Copyright (C) 2016 Cirilo Bernardo <[email protected]>
6 * Copyright The 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
26#include <algorithm>
27#include <cmath>
28#include <sstream>
29#include <string>
30#include <utility>
31#include <wx/file.h>
32#include <wx/filename.h>
33#include <wx/filefn.h>
34#include <wx/sstream.h>
35#include <wx/stdpaths.h>
36#include <wx/stream.h>
37#include <wx/string.h>
38#include <wx/zstream.h>
39#include <wx/wfstream.h>
40#include <wx/zipstrm.h>
41#include <wx/stdstream.h>
42#include <wx/crt.h>
43
44#include <decompress.hpp>
45
46#include <thread_pool.h>
47#include <board.h>
49#include <footprint.h>
50#include <pad.h>
51#include <pcb_track.h>
52#include <kiplatform/io.h>
53#include <string_utils.h>
54#include <build_version.h>
59#include <reporter.h>
60
61#include "step_pcb_model.h"
62#include "streamwrapper.h"
63
64#include <IGESCAFControl_Reader.hxx>
65#include <IGESCAFControl_Writer.hxx>
66#include <IGESControl_Controller.hxx>
67#include <IGESData_GlobalSection.hxx>
68#include <IGESData_IGESModel.hxx>
69#include <Interface_Static.hxx>
70#include <Quantity_Color.hxx>
71#include <STEPCAFControl_Reader.hxx>
72#include <STEPCAFControl_Writer.hxx>
73#include <APIHeaderSection_MakeHeader.hxx>
74#include <Standard_Failure.hxx>
75#include <Standard_Handle.hxx>
76#include <Standard_Version.hxx>
77#include <TCollection_ExtendedString.hxx>
78#include <TDocStd_Document.hxx>
79#include <TDocStd_XLinkTool.hxx>
80#include <TDataStd_Name.hxx>
81#include <TDataStd_TreeNode.hxx>
82#include <TDF_LabelSequence.hxx>
83#include <TDF_Tool.hxx>
84#include <TopExp_Explorer.hxx>
85#include <TopoDS.hxx>
86#include <XCAFApp_Application.hxx>
87#include <XCAFDoc.hxx>
88#include <XCAFDoc_DocumentTool.hxx>
89#include <XCAFDoc_ColorTool.hxx>
90#include <XCAFDoc_ShapeTool.hxx>
91#include <XCAFDoc_VisMaterialTool.hxx>
92#include <XCAFDoc_Area.hxx>
93#include <XCAFDoc_Centroid.hxx>
94#include <XCAFDoc_Location.hxx>
95#include <XCAFDoc_Volume.hxx>
96
97#include "KI_XCAFDoc_AssemblyGraph.hxx"
98
99#include <BRep_Tool.hxx>
100#include <BRepMesh_IncrementalMesh.hxx>
101#include <BRepBuilderAPI_GTransform.hxx>
102#include <BRepBuilderAPI_MakeEdge.hxx>
103#include <BRepBuilderAPI_MakeWire.hxx>
104#include <BRepBuilderAPI_MakeFace.hxx>
105#include <BRepExtrema_DistShapeShape.hxx>
106#include <BRepPrimAPI_MakePrism.hxx>
107#include <BRepTools.hxx>
108#include <BRepLib_MakeWire.hxx>
109#include <BRepAdaptor_Surface.hxx>
110#include <BRepAlgoAPI_Check.hxx>
111#include <BRepAlgoAPI_Cut.hxx>
112#include <BRepAlgoAPI_Fuse.hxx>
113#include <ShapeUpgrade_UnifySameDomain.hxx>
114
115#include <BRepBndLib.hxx>
116#include <Bnd_BoundSortBox.hxx>
117#include <GProp_GProps.hxx>
118#include <BRepGProp.hxx>
119
120#include <Geom_Curve.hxx>
121#include <Geom_TrimmedCurve.hxx>
122
123#include <gp_Ax2.hxx>
124#include <gp_Dir.hxx>
125#include <gp_Pnt.hxx>
126#include <GC_MakeArcOfCircle.hxx>
127#include <GC_MakeCircle.hxx>
128
129#include <RWGltf_CafWriter.hxx>
130#include <StlAPI_Writer.hxx>
131
132#if OCC_VERSION_HEX >= 0x070700
133#include <VrmlAPI_CafReader.hxx>
134#include <RWPly_CafWriter.hxx>
135#endif
136
137#include <macros.h>
139
140static constexpr double USER_PREC = 1e-4;
141static constexpr double USER_ANGLE_PREC = 1e-6;
142
143// nominal offset from the board
144static constexpr double BOARD_OFFSET = 0.05;
145
146// supported file types for 3D models
148{
156 FMT_WRZ
158
159
160MODEL3D_FORMAT_TYPE fileType( const char* aFileName )
161{
162 wxFileName lfile( wxString::FromUTF8Unchecked( aFileName ) );
163
164 if( !lfile.FileExists() )
165 return FMT_NONE;
166
167 wxString ext = lfile.GetExt().Lower();
168
169 if( ext == wxT( "wrl" ) )
170 return FMT_WRL;
171
172 if( ext == wxT( "wrz" ) )
173 return FMT_WRZ;
174
175 if( ext == wxT( "idf" ) )
176 return FMT_IDF; // component outline
177
178 if( ext == wxT( "emn" ) )
179 return FMT_EMN; // PCB assembly
180
181 if( ext == wxT( "stpz" ) || ext == wxT( "gz" ) )
182 return FMT_STEPZ;
183
184 OPEN_ISTREAM( ifile, aFileName );
185
186 if( ifile.fail() )
187 return FMT_NONE;
188
189 char iline[82];
190 MODEL3D_FORMAT_TYPE format_type = FMT_NONE;
191
192 // The expected header should be the first line.
193 // However some files can have a comment at the beginning of the file
194 // So read up to max_line_count lines to try to find the actual header
195 const int max_line_count = 3;
196
197 for( int ii = 0; ii < max_line_count; ii++ )
198 {
199 memset( iline, 0, 82 );
200 ifile.getline( iline, 82 );
201
202 iline[81] = 0; // ensure NULL termination when string is too long
203
204 // check for STEP in Part 21 format
205 // (this can give false positives since Part 21 is not exclusively STEP)
206 if( !strncmp( iline, "ISO-10303-21;", 13 ) )
207 {
208 format_type = FMT_STEP;
209 break;
210 }
211
212 std::string fstr = iline;
213
214 // check for STEP in XML format
215 // (this can give both false positive and false negatives)
216 if( fstr.find( "urn:oid:1.0.10303." ) != std::string::npos )
217 {
218 format_type = FMT_STEP;
219 break;
220 }
221
222 // Note: this is a very simple test which can yield false positives; the only
223 // sure method for determining if a file *not* an IGES model is to attempt
224 // to load it.
225 if( iline[72] == 'S' && ( iline[80] == 0 || iline[80] == 13 || iline[80] == 10 ) )
226 {
227 format_type = FMT_IGES;
228 break;
229 }
230
231 // Only a comment (starting by "/*") is allowed as header
232 if( strncmp( iline, "/*", 2 ) != 0 ) // not a comment
233 break;
234 }
235
236 CLOSE_STREAM( ifile );
237
238 return format_type;
239}
240
241
243 const VECTOR2D& p3 )
244{
246
247 // Move coordinate origin to p2, to simplify calculations
248 VECTOR2D b = p1 - p2;
249 VECTOR2D d = p3 - p2;
250 double bc = ( b.x * b.x + b.y * b.y ) / 2.0;
251 double cd = ( -d.x * d.x - d.y * d.y ) / 2.0;
252 double det = -b.x * d.y + d.x * b.y;
253
254 // We're fine with divisions by 0
255 det = 1.0 / det;
256 center.x = ( -bc * d.y - cd * b.y ) * det;
257 center.y = ( b.x * cd + d.x * bc ) * det;
258 center += p2;
259
260 return center;
261}
262
263
264#define APPROX_DBG( stmt )
265//#define APPROX_DBG( stmt ) stmt
266
268{
269 // An algo that takes 3 points, calculates a circle center,
270 // then tries to find as many points fitting the circle.
271
272 static const double c_radiusDeviation = 1000.0;
273 static const double c_arcCenterDeviation = 1000.0;
274 static const double c_relLengthDeviation = 0.8;
275 static const int c_last_none = -1000; // Meaning the arc cannot be constructed
276 // Allow larger angles for segments below this size
277 static const double c_smallSize = pcbIUScale.mmToIU( 0.1 );
278 static const double c_circleCloseGap = pcbIUScale.mmToIU( 1.0 );
279
280 APPROX_DBG( std::cout << std::endl );
281
282 if( aSrc.PointCount() < 4 )
283 return aSrc;
284
285 if( !aSrc.IsClosed() )
286 return aSrc; // non-closed polygons are not supported
287
289
290 int jEndIdx = aSrc.PointCount() - 3;
291
292 for( int i = 0; i < aSrc.PointCount(); i++ )
293 {
294 int first = i - 3;
295 int last = c_last_none;
296
297 VECTOR2D p0 = aSrc.CPoint( i - 3 );
298 VECTOR2D p1 = aSrc.CPoint( i - 2 );
299 VECTOR2D p2 = aSrc.CPoint( i - 1 );
300
301 APPROX_DBG( std::cout << i << " " << aSrc.CPoint( i ) << " " << ( i - 3 ) << " "
302 << VECTOR2I( p0 ) << " " << ( i - 2 ) << " " << VECTOR2I( p1 ) << " "
303 << ( i - 1 ) << " " << VECTOR2I( p2 ) << std::endl );
304
305 VECTOR2D v01 = p1 - p0;
306 VECTOR2D v12 = p2 - p1;
307
308 bool defective = false;
309
310 double d01 = v01.EuclideanNorm();
311 double d12 = v12.EuclideanNorm();
312
313 // Check distance differences between 3 first points
314 defective |= std::abs( d01 - d12 ) > ( std::max( d01, d12 ) * c_relLengthDeviation );
315
316 if( !defective )
317 {
318 // Check angles between 3 first points
319 EDA_ANGLE a01( v01 );
320 EDA_ANGLE a12( v12 );
321
322 double a_diff = ( a01 - a12 ).Normalize180().AsDegrees();
323 defective |= std::abs( a_diff ) < 0.1;
324
325 // Larger angles are allowed for smaller geometry
326 double maxAngleDiff = std::max( d01, d12 ) < c_smallSize ? 46.0 : 30.0;
327 defective |= std::abs( a_diff ) >= maxAngleDiff;
328 }
329
330 if( !defective )
331 {
332 // Find last point lying on the circle created from 3 first points
334 double radius = ( p0 - center ).EuclideanNorm();
335 VECTOR2D p_prev = p2;
336 EDA_ANGLE a_prev( v12 );
337
338 for( int j = i; j <= jEndIdx; j++ )
339 {
340 VECTOR2D p_test = aSrc.CPoint( j );
341
342 EDA_ANGLE a_test( p_test - p_prev );
343 double rad_test = ( p_test - center ).EuclideanNorm();
344 double d_tl = ( p_test - p_prev ).EuclideanNorm();
345 double rad_dev = std::abs( radius - rad_test );
346
347 APPROX_DBG( std::cout << " " << j << " " << aSrc.CPoint( j ) << " rad "
348 << int64_t( rad_test ) << " ref " << int64_t( radius )
349 << std::endl );
350
351 if( rad_dev > c_radiusDeviation )
352 {
353 APPROX_DBG( std::cout << " " << j
354 << " Radius deviation too large: " << int64_t( rad_dev )
355 << " > " << c_radiusDeviation << std::endl );
356 break;
357 }
358
359 // Larger angles are allowed for smaller geometry
360 double maxAngleDiff =
361 std::max( std::max( d01, d12 ), d_tl ) < c_smallSize ? 46.0 : 30.0;
362
363 double a_diff_test = ( a_prev - a_test ).Normalize180().AsDegrees();
364 if( std::abs( a_diff_test ) >= maxAngleDiff )
365 {
366 APPROX_DBG( std::cout << " " << j << " Angles differ too much " << a_diff_test
367 << std::endl );
368 break;
369 }
370
371 if( std::abs( d_tl - d01 ) > ( std::max( d_tl, d01 ) * c_relLengthDeviation ) )
372 {
373 APPROX_DBG( std::cout << " " << j << " Lengths differ too much " << d_tl
374 << "; " << d01 << std::endl );
375 break;
376 }
377
378 last = j;
379 p_prev = p_test;
380 a_prev = a_test;
381 }
382 }
383
384 if( last != c_last_none )
385 {
386 // Try to add an arc, testing for self-interference
387 SHAPE_ARC arc( aSrc.CPoint( first ), aSrc.CPoint( ( first + last ) / 2 ),
388 aSrc.CPoint( last ), 0 );
389
390 if( last > aSrc.PointCount() - 3 && !dst.IsArcSegment( 0 ) )
391 {
392 // If we've found an arc at the end, but already added segments at the start, remove them.
393 int toRemove = last - ( aSrc.PointCount() - 3 );
394
395 while( toRemove )
396 {
397 dst.RemoveShape( 0 );
398 toRemove--;
399 }
400 }
401
402 SHAPE_LINE_CHAIN testChain = dst;
403
404 testChain.Append( arc );
405 testChain.Append( aSrc.Slice( last, std::max( last, aSrc.PointCount() - 3 ) ) );
406 testChain.SetClosed( aSrc.IsClosed() );
407
408 if( !testChain.SelfIntersectingWithArcs() )
409 {
410 // Add arc
411 dst.Append( arc );
412
413 APPROX_DBG( std::cout << " Add arc start " << arc.GetP0() << " mid "
414 << arc.GetArcMid() << " end " << arc.GetP1() << std::endl );
415
416 i = last + 3;
417 }
418 else
419 {
420 // Self-interference
421 last = c_last_none;
422
423 APPROX_DBG( std::cout << " Self-intersection check failed" << std::endl );
424 }
425 }
426
427 if( last == c_last_none )
428 {
429 if( first < 0 )
430 jEndIdx = first + aSrc.PointCount();
431
432 // Add point
433 dst.Append( p0 );
434 APPROX_DBG( std::cout << " Add pt " << VECTOR2I( p0 ) << std::endl );
435 }
436 }
437
438 dst.SetClosed( true );
439
440 // Try to merge arcs
441 int iarc0 = dst.ArcIndex( 0 );
442 int iarc1 = dst.ArcIndex( dst.GetSegmentCount() - 1 );
443
444 if( iarc0 != -1 && iarc1 != -1 )
445 {
446 APPROX_DBG( std::cout << "Final arcs " << iarc0 << " " << iarc1 << std::endl );
447
448 if( iarc0 == iarc1 )
449 {
450 SHAPE_ARC arc = dst.Arc( iarc0 );
451
452 VECTOR2D p0 = arc.GetP0();
453 VECTOR2D p1 = arc.GetP1();
454
455 // If we have only one arc and the gap is small, make it a circle
456 if( ( p1 - p0 ).EuclideanNorm() < c_circleCloseGap )
457 {
458 dst.Clear();
459 dst.Append( SHAPE_ARC( arc.GetCenter(), arc.GetP0(), ANGLE_360 ) );
460 }
461 }
462 else
463 {
464 // Merge first and last arcs if they are similar
465 SHAPE_ARC arc0 = dst.Arc( iarc0 );
466 SHAPE_ARC arc1 = dst.Arc( iarc1 );
467
468 VECTOR2D ac0 = arc0.GetCenter();
469 VECTOR2D ac1 = arc1.GetCenter();
470
471 double ar0 = arc0.GetRadius();
472 double ar1 = arc1.GetRadius();
473
474 if( std::abs( ar0 - ar1 ) <= c_radiusDeviation
475 && ( ac0 - ac1 ).EuclideanNorm() <= c_arcCenterDeviation )
476 {
477 dst.RemoveShape( 0 );
478 dst.RemoveShape( -1 );
479
480 SHAPE_ARC merged( arc1.GetP0(), arc1.GetArcMid(), arc0.GetP1(), 0 );
481
482 dst.Append( merged );
483 }
484 }
485 }
486
487 return dst;
488}
489
490
491static TopoDS_Shape getOneShape( Handle( XCAFDoc_ShapeTool ) aShapeTool )
492{
493 TDF_LabelSequence theLabels;
494 aShapeTool->GetFreeShapes( theLabels );
495
496 TopoDS_Shape aShape;
497
498 if( theLabels.Length() == 1 )
499 return aShapeTool->GetShape( theLabels.Value( 1 ) );
500
501 TopoDS_Compound aCompound;
502 BRep_Builder aBuilder;
503 aBuilder.MakeCompound( aCompound );
504
505 for( TDF_LabelSequence::Iterator anIt( theLabels ); anIt.More(); anIt.Next() )
506 {
507 TopoDS_Shape aFreeShape;
508
509 if( !aShapeTool->GetShape( anIt.Value(), aFreeShape ) )
510 continue;
511
512 aBuilder.Add( aCompound, aFreeShape );
513 }
514
515 if( aCompound.NbChildren() > 0 )
516 aShape = aCompound;
517
518 return aShape;
519}
520
521
522// Apply scaling to shapes within theLabel.
523// Based on XCAFDoc_Editor::RescaleGeometry
524static Standard_Boolean rescaleShapes( const TDF_Label& theLabel, const gp_XYZ& aScale )
525{
526 if( theLabel.IsNull() )
527 {
528 Message::SendFail( "Null label." );
529 return Standard_False;
530 }
531
532 if( Abs( aScale.X() ) <= gp::Resolution() || Abs( aScale.Y() ) <= gp::Resolution()
533 || Abs( aScale.Z() ) <= gp::Resolution() )
534 {
535 Message::SendFail( "Scale factor is too small." );
536 return Standard_False;
537 }
538
539 Handle( XCAFDoc_ShapeTool ) aShapeTool = XCAFDoc_DocumentTool::ShapeTool( theLabel );
540
541 if( aShapeTool.IsNull() )
542 {
543 Message::SendFail( "Couldn't find XCAFDoc_ShapeTool attribute." );
544 return Standard_False;
545 }
546
547 Handle( KI_XCAFDoc_AssemblyGraph ) aG = new KI_XCAFDoc_AssemblyGraph( theLabel );
548
549 if( aG.IsNull() )
550 {
551 Message::SendFail( "Couldn't create assembly graph." );
552 return Standard_False;
553 }
554
555 Standard_Boolean anIsDone = Standard_True;
556
557 // clang-format off
558 gp_GTrsf aGTrsf;
559 aGTrsf.SetVectorialPart( gp_Mat( aScale.X(), 0, 0,
560 0, aScale.Y(), 0,
561 0, 0, aScale.Z() ) );
562 // clang-format on
563
564 BRepBuilderAPI_GTransform aBRepTrsf( aGTrsf );
565
566 for( Standard_Integer idx = 1; idx <= aG->NbNodes(); idx++ )
567 {
568 const KI_XCAFDoc_AssemblyGraph::NodeType aNodeType = aG->GetNodeType( idx );
569
570 if( ( aNodeType != KI_XCAFDoc_AssemblyGraph::NodeType_Part )
571 && ( aNodeType != KI_XCAFDoc_AssemblyGraph::NodeType_Occurrence ) )
572 {
573 continue;
574 }
575
576 const TDF_Label& aLabel = aG->GetNode( idx );
577
578 if( aNodeType == KI_XCAFDoc_AssemblyGraph::NodeType_Part )
579 {
580 const TopoDS_Shape aShape = aShapeTool->GetShape( aLabel );
581 aBRepTrsf.Perform( aShape, Standard_True );
582 if( !aBRepTrsf.IsDone() )
583 {
584 Standard_SStream aSS;
585 TCollection_AsciiString anEntry;
586 TDF_Tool::Entry( aLabel, anEntry );
587 aSS << "Shape " << anEntry << " is not scaled!";
588 Message::SendFail( aSS.str().c_str() );
589 anIsDone = Standard_False;
590 return Standard_False;
591 }
592 TopoDS_Shape aScaledShape = aBRepTrsf.Shape();
593 aShapeTool->SetShape( aLabel, aScaledShape );
594
595 // Update sub-shapes
596 TDF_LabelSequence aSubshapes;
597 aShapeTool->GetSubShapes( aLabel, aSubshapes );
598 for( TDF_LabelSequence::Iterator anItSs( aSubshapes ); anItSs.More(); anItSs.Next() )
599 {
600 const TDF_Label& aLSs = anItSs.Value();
601 const TopoDS_Shape aSs = aShapeTool->GetShape( aLSs );
602 const TopoDS_Shape aSs1 = aBRepTrsf.ModifiedShape( aSs );
603 aShapeTool->SetShape( aLSs, aSs1 );
604 }
605
606 // These attributes will be recomputed eventually, but clear them just in case
607 aLabel.ForgetAttribute( XCAFDoc_Area::GetID() );
608 aLabel.ForgetAttribute( XCAFDoc_Centroid::GetID() );
609 aLabel.ForgetAttribute( XCAFDoc_Volume::GetID() );
610 }
611 else if( aNodeType == KI_XCAFDoc_AssemblyGraph::NodeType_Occurrence )
612 {
613 TopLoc_Location aLoc = aShapeTool->GetLocation( aLabel );
614 gp_Trsf aTrsf = aLoc.Transformation();
615 aTrsf.SetTranslationPart( aTrsf.TranslationPart().Multiplied( aScale ) );
616 XCAFDoc_Location::Set( aLabel, aTrsf );
617 }
618 }
619
620 if( !anIsDone )
621 {
622 return Standard_False;
623 }
624
625 aShapeTool->UpdateAssemblies();
626
627 return anIsDone;
628}
629
630
631static bool fuseShapes( auto& aInputShapes, TopoDS_Shape& aOutShape, REPORTER* aReporter )
632{
633 BRepAlgoAPI_Fuse mkFuse;
634 TopTools_ListOfShape shapeArguments, shapeTools;
635
636 for( TopoDS_Shape& sh : aInputShapes )
637 {
638 if( sh.IsNull() )
639 continue;
640
641 if( shapeArguments.IsEmpty() )
642 shapeArguments.Append( sh );
643 else
644 shapeTools.Append( sh );
645 }
646
647 mkFuse.SetRunParallel( true );
648 mkFuse.SetToFillHistory( false );
649 mkFuse.SetArguments( shapeArguments );
650 mkFuse.SetTools( shapeTools );
651 mkFuse.Build();
652
653 if( mkFuse.HasErrors() || mkFuse.HasWarnings() )
654 {
655 aReporter->Report( _( "** Got problems while fusing shapes **\n" ), RPT_SEVERITY_ERROR );
656
657 if( mkFuse.HasErrors() )
658 {
659 wxString msg = _( "Errors:\n" );
660 wxStringOutputStream os_stream( &msg );
661 wxStdOutputStream out( os_stream );
662
663 mkFuse.DumpErrors( out );
664 aReporter->Report( msg, RPT_SEVERITY_ERROR );
665 }
666
667 if( mkFuse.HasWarnings() )
668 {
669 wxString msg = _( "Warnings:\n" );
670 wxStringOutputStream os_stream( &msg );
671 wxStdOutputStream out( os_stream );
672
673 mkFuse.DumpWarnings( out );
674 aReporter->Report( msg, RPT_SEVERITY_WARNING );
675 }
676 }
677
678 if( mkFuse.IsDone() )
679 {
680 TopoDS_Shape fusedShape = mkFuse.Shape();
681
682 ShapeUpgrade_UnifySameDomain unify( fusedShape, true, true, false );
683 unify.History() = nullptr;
684 unify.Build();
685
686 TopoDS_Shape unifiedShapes = unify.Shape();
687
688 if( unifiedShapes.IsNull() )
689 {
690 aReporter->Report( _( "** ShapeUpgrade_UnifySameDomain produced a null shape **\n" ),
692 }
693 else
694 {
695 aOutShape = unifiedShapes;
696 return true;
697 }
698 }
699
700 return false;
701}
702
703
704static TopoDS_Compound makeCompound( const auto& aInputShapes )
705{
706 TopoDS_Compound compound;
707 BRep_Builder builder;
708 builder.MakeCompound( compound );
709
710 for( const TopoDS_Shape& shape : aInputShapes )
711 builder.Add( compound, shape );
712
713 return compound;
714}
715
716
717// Try to fuse shapes. If that fails, just add them to a compound
718static TopoDS_Shape fuseShapesOrCompound( const TopTools_ListOfShape& aInputShapes,
719 REPORTER* aReporter )
720{
721 TopoDS_Shape outShape;
722
723 if( aInputShapes.Size() == 1 )
724 return aInputShapes.First();
725
726 if( fuseShapes( aInputShapes, outShape, aReporter ) )
727 return outShape;
728
729 return makeCompound( aInputShapes );
730}
731
732
733// Sets names in assembly to <aPrefix> (<old name>), or to <aPrefix>
734static Standard_Boolean prefixNames( const TDF_Label& aLabel,
735 const TCollection_ExtendedString& aPrefix )
736{
737 Handle( KI_XCAFDoc_AssemblyGraph ) aG = new KI_XCAFDoc_AssemblyGraph( aLabel );
738
739 if( aG.IsNull() )
740 {
741 Message::SendFail( "Couldn't create assembly graph." );
742 return Standard_False;
743 }
744
745 Standard_Boolean anIsDone = Standard_True;
746
747 for( Standard_Integer idx = 1; idx <= aG->NbNodes(); idx++ )
748 {
749 const TDF_Label& lbl = aG->GetNode( idx );
750 Handle( TDataStd_Name ) nameHandle;
751
752 if( lbl.FindAttribute( TDataStd_Name::GetID(), nameHandle ) )
753 {
754 TCollection_ExtendedString name;
755
756 name += aPrefix;
757 name += " (";
758 name += nameHandle->Get();
759 name += ")";
760
761 TDataStd_Name::Set( lbl, name );
762 }
763 else
764 {
765 TDataStd_Name::Set( lbl, aPrefix );
766 }
767 }
768
769 return anIsDone;
770}
771
772
773STEP_PCB_MODEL::STEP_PCB_MODEL( const wxString& aPcbName, REPORTER* aReporter ) :
774 m_reporter( aReporter )
775{
776 m_app = XCAFApp_Application::GetApplication();
777 m_app->NewDocument( "MDTV-XCAF", m_doc );
778 m_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
779 m_assy_label = m_assy->NewShape();
780 m_hasPCB = false;
781 m_simplifyShapes = true;
782 m_components = 0;
786 m_minx = 1.0e10; // absurdly large number; any valid PCB X value will be smaller
787 m_pcbName = aPcbName;
788 m_fuseShapes = false;
789 m_outFmt = OUTPUT_FORMAT::FMT_OUT_UNKNOWN;
790}
791
792
794{
795 if( m_doc->CanClose() == CDM_CCS_OK )
796 m_doc->Close();
797}
798
799
800bool STEP_PCB_MODEL::AddPadShape( const PAD* aPad, const VECTOR2D& aOrigin, bool aVia,
801 SHAPE_POLY_SET* aClipPolygon )
802{
803 const double c_padExtraThickness = 0.005;
804 bool success = true;
805 std::vector<TopoDS_Shape> padShapes;
806 bool castellated = aClipPolygon && aPad->GetProperty() == PAD_PROP::CASTELLATED;
807
808 for( PCB_LAYER_ID pcb_layer : aPad->GetLayerSet().Seq() )
809 {
810 if( !m_enabledLayers.Contains( pcb_layer ) )
811 continue;
812
813 if( pcb_layer == F_Mask || pcb_layer == B_Mask )
814 continue;
815
816 if( !aPad->FlashLayer( pcb_layer ) )
817 continue;
818
819 double Zpos, thickness;
820 getLayerZPlacement( pcb_layer, Zpos, thickness );
821
822 if( !aVia )
823 {
824 // Pad surface as a separate face for FEM simulations.
825 if( pcb_layer == F_Cu )
826 thickness += c_padExtraThickness;
827 else if( pcb_layer == B_Cu )
828 thickness -= c_padExtraThickness;
829 }
830
831 TopoDS_Shape testShape;
832
833 // Make a shape on copper layers
834 SHAPE_POLY_SET polySet;
835 aPad->TransformShapeToPolygon( polySet, pcb_layer, 0, aPad->GetMaxError(), ERROR_INSIDE );
836
837 if( castellated )
838 {
839 polySet.ClearArcs();
840 polySet.BooleanIntersection( *aClipPolygon );
841 }
842
843 success &= MakeShapes( padShapes, polySet, m_simplifyShapes, thickness, Zpos, aOrigin );
844
845 if( testShape.IsNull() )
846 {
847 std::vector<TopoDS_Shape> testShapes;
848
849 MakeShapes( testShapes, polySet, m_simplifyShapes, 0.0, Zpos + thickness, aOrigin );
850
851 if( testShapes.size() > 0 )
852 testShape = testShapes.front();
853 }
854
855 if( !aVia && !testShape.IsNull() )
856 {
857 if( pcb_layer == F_Cu || pcb_layer == B_Cu )
858 {
859 wxString name;
860
861 name << "Pad_";
862
863 if( pcb_layer == F_Cu )
864 name << 'F' << '_';
865 else if( pcb_layer == B_Cu )
866 name << 'B' << '_';
867
868 name << aPad->GetParentFootprint()->GetReferenceAsString() << '_'
869 << aPad->GetNumber() << '_' << aPad->GetShortNetname();
870
871 gp_Pnt point( pcbIUScale.IUTomm( aPad->GetX() - aOrigin.x ),
872 -pcbIUScale.IUTomm( aPad->GetY() - aOrigin.y ), Zpos + thickness );
873
874 m_pad_points[name].emplace_back( point, testShape );
875 }
876 }
877 }
878
879 if( aPad->GetAttribute() == PAD_ATTRIB::PTH && aPad->IsOnLayer( F_Cu ) && aPad->IsOnLayer( B_Cu ) )
880 {
881 double f_pos, f_thickness;
882 double b_pos, b_thickness;
883 getLayerZPlacement( F_Cu, f_pos, f_thickness );
884 getLayerZPlacement( B_Cu, b_pos, b_thickness );
885
886 if( !aVia )
887 {
888 // Pad surface is slightly thicker
889 f_thickness += c_padExtraThickness;
890 b_thickness -= c_padExtraThickness;
891 }
892
893 double top = std::max( f_pos, f_pos + f_thickness );
894 double bottom = std::min( b_pos, b_pos + b_thickness );
895 double hole_height = top - bottom;
896
897 TopoDS_Shape plating;
898
899 std::shared_ptr<SHAPE_SEGMENT> seg_hole = aPad->GetEffectiveHoleShape();
900 double width = std::min( aPad->GetDrillSize().x, aPad->GetDrillSize().y );
901
902 if( !castellated )
903 {
904 if( MakeShapeAsThickSegment( plating, seg_hole->GetSeg().A, seg_hole->GetSeg().B, width,
905 hole_height, bottom, aOrigin ) )
906 {
907 padShapes.push_back( plating );
908 }
909 else
910 {
911 success = false;
912 }
913 }
914 else
915 {
916 // Note:
917 // the truncated hole shape is exported as a vertical filled shape. The hole itself
918 // will be removed later, when all holes are removed from the board
919 SHAPE_POLY_SET polyHole;
920
921 if( seg_hole->GetSeg().A == seg_hole->GetSeg().B ) // Hole is a circle
922 {
923 TransformCircleToPolygon( polyHole, seg_hole->GetSeg().A, width/2,
924 aPad->GetMaxError(), ERROR_OUTSIDE );
925
926 }
927 else
928 {
929 TransformOvalToPolygon( polyHole, seg_hole->GetSeg().A, seg_hole->GetSeg().B, width,
930 aPad->GetMaxError(), ERROR_OUTSIDE );
931 }
932
933 polyHole.ClearArcs();
934 polyHole.BooleanIntersection( *aClipPolygon );
935
936 if( MakePolygonAsWall( plating, polyHole, hole_height, bottom, aOrigin ) )
937 {
938 padShapes.push_back( plating );
939 }
940 else
941 {
942 success = false;
943 }
944 }
945 }
946
947 if( !success ) // Error
948 m_reporter->Report( _( "OCC error adding pad/via polygon.\n" ), RPT_SEVERITY_ERROR );
949
950 if( !padShapes.empty() )
951 {
952 // Fuse pad shapes here before fusing them with tracks because OCCT sometimes has trouble
953 if( m_fuseShapes )
954 {
955 TopTools_ListOfShape padShapesList;
956
957 for( const TopoDS_Shape& shape : padShapes )
958 padShapesList.Append( shape );
959
960 m_board_copper_pads[aPad->GetNetname()].push_back( fuseShapesOrCompound( padShapesList, m_reporter ) );
961 }
962 else
963 {
964 for( const TopoDS_Shape& shape : padShapes )
965 m_board_copper_pads[aPad->GetNetname()].push_back( shape );
966 }
967 }
968
969 return success;
970}
971
972
973bool STEP_PCB_MODEL::AddHole( const SHAPE_SEGMENT& aShape, int aPlatingThickness,
974 PCB_LAYER_ID aLayerTop, PCB_LAYER_ID aLayerBot, bool aVia,
975 const VECTOR2D& aOrigin, bool aCutCopper, bool aCutBody )
976{
977 double margin = 0.001; // a small margin on the Z axix to be sure the hole
978 // is bigger than the board with copper
979 // must be > OCC_MAX_DISTANCE_TO_MERGE_POINTS
980
981 // Pads are taller by 0.01 mm
982 if( !aVia )
983 margin += 0.01;
984
985 double f_pos, f_thickness;
986 double b_pos, b_thickness;
987 getLayerZPlacement( aLayerTop, f_pos, f_thickness );
988 getLayerZPlacement( aLayerBot, b_pos, b_thickness );
989 double top = std::max( f_pos, f_pos + f_thickness );
990 double bottom = std::min( b_pos, b_pos + b_thickness );
991
992 double holeZsize = ( top - bottom ) + ( margin * 2 );
993
994 double boardDrill = aShape.GetWidth();
995 double copperDrill = boardDrill - aPlatingThickness * 2;
996
997 TopoDS_Shape copperHole, boardHole;
998
999 if( aCutCopper )
1000 {
1001 if( MakeShapeAsThickSegment( copperHole, aShape.GetSeg().A, aShape.GetSeg().B, copperDrill,
1002 holeZsize, bottom - margin, aOrigin ) )
1003 {
1004 m_copperCutouts.push_back( copperHole );
1005 }
1006 else
1007 {
1008 return false;
1009 }
1010 }
1011
1012 if( aCutBody )
1013 {
1014 if( MakeShapeAsThickSegment( boardHole, aShape.GetSeg().A, aShape.GetSeg().B, boardDrill,
1015 holeZsize, bottom - margin, aOrigin ) )
1016 {
1017 m_boardCutouts.push_back( boardHole );
1018 }
1019 else
1020 {
1021 return false;
1022 }
1023 }
1024
1025 return true;
1026}
1027
1028
1030 PCB_LAYER_ID aLayerBot, bool aVia, const VECTOR2D& aOrigin,
1031 const wxString& aNetname )
1032{
1033 double f_pos, f_thickness;
1034 double b_pos, b_thickness;
1035 getLayerZPlacement( aLayerTop, f_pos, f_thickness );
1036 getLayerZPlacement( aLayerBot, b_pos, b_thickness );
1037 double top = std::max( f_pos, f_pos + f_thickness );
1038 double bottom = std::min( b_pos, b_pos + b_thickness );
1039
1040 TopoDS_Shape plating;
1041
1042 if( !MakeShapeAsThickSegment( plating, aShape.GetSeg().A, aShape.GetSeg().B, aShape.GetWidth(),
1043 ( top - bottom ), bottom, aOrigin ) )
1044 {
1045 return false;
1046 }
1047
1048 if( aVia )
1049 m_board_copper_vias[aNetname].push_back( plating );
1050 else
1051 m_board_copper_pads[aNetname].push_back( plating );
1052
1053 return true;
1054}
1055
1056
1057void STEP_PCB_MODEL::getLayerZPlacement( const PCB_LAYER_ID aLayer, double& aZPos,
1058 double& aThickness )
1059{
1060 // Offsets above copper in mm
1061 static const double c_silkscreenAboveCopper = 0.04;
1062 static const double c_soldermaskAboveCopper = 0.015;
1063
1064 if( IsCopperLayer( aLayer ) )
1065 {
1066 getCopperLayerZPlacement( aLayer, aZPos, aThickness );
1067 }
1068 else if( IsFrontLayer( aLayer ) )
1069 {
1070 double f_pos, f_thickness;
1071 getCopperLayerZPlacement( F_Cu, f_pos, f_thickness );
1072 double top = std::max( f_pos, f_pos + f_thickness );
1073
1074 if( aLayer == F_SilkS )
1075 aZPos = top + c_silkscreenAboveCopper;
1076 else
1077 aZPos = top + c_soldermaskAboveCopper;
1078
1079 aThickness = 0.0; // Normal points up
1080 }
1081 else if( IsBackLayer( aLayer ) )
1082 {
1083 double b_pos, b_thickness;
1084 getCopperLayerZPlacement( B_Cu, b_pos, b_thickness );
1085 double bottom = std::min( b_pos, b_pos + b_thickness );
1086
1087 if( aLayer == B_SilkS )
1088 aZPos = bottom - c_silkscreenAboveCopper;
1089 else
1090 aZPos = bottom - c_soldermaskAboveCopper;
1091
1092 aThickness = -0.0; // Normal points down
1093 }
1094}
1095
1096
1098 double& aThickness )
1099{
1100 int z = 0;
1101 int thickness = 0;
1102 bool wasPrepreg = false;
1103
1104 const std::vector<BOARD_STACKUP_ITEM*>& materials = m_stackup.GetList();
1105
1106 // Iterate from bottom to top
1107 for( auto it = materials.rbegin(); it != materials.rend(); ++it )
1108 {
1109 const BOARD_STACKUP_ITEM* item = *it;
1110
1111 if( item->GetType() == BS_ITEM_TYPE_COPPER )
1112 {
1113 if( aLayer == B_Cu )
1114 {
1115 // This is the first encountered layer
1116 thickness = -item->GetThickness();
1117 break;
1118 }
1119
1120 // Inner copper position is usually inside prepreg
1121 if( wasPrepreg && item->GetBrdLayerId() != F_Cu )
1122 {
1123 z += item->GetThickness();
1124 thickness = -item->GetThickness();
1125 }
1126 else
1127 {
1128 thickness = item->GetThickness();
1129 }
1130
1131 if( item->GetBrdLayerId() == aLayer )
1132 break;
1133
1134 if( !wasPrepreg && item->GetBrdLayerId() != B_Cu )
1135 z += item->GetThickness();
1136 }
1137 else if( item->GetType() == BS_ITEM_TYPE_DIELECTRIC )
1138 {
1139 wasPrepreg = ( item->GetTypeName() == KEY_PREPREG );
1140
1141 // Dielectric can have sub-layers. Layer 0 is the main layer
1142 // Not frequent, but possible
1143 thickness = 0;
1144 for( int idx = 0; idx < item->GetSublayersCount(); idx++ )
1145 thickness += item->GetThickness( idx );
1146
1147 z += thickness;
1148 }
1149 }
1150
1151 aZPos = pcbIUScale.IUTomm( z );
1152 aThickness = pcbIUScale.IUTomm( thickness );
1153}
1154
1155
1156void STEP_PCB_MODEL::getBoardBodyZPlacement( double& aZPos, double& aThickness )
1157{
1158 double f_pos, f_thickness;
1159 double b_pos, b_thickness;
1160 getLayerZPlacement( F_Cu, f_pos, f_thickness );
1161 getLayerZPlacement( B_Cu, b_pos, b_thickness );
1162 double top = std::min( f_pos, f_pos + f_thickness );
1163 double bottom = std::max( b_pos, b_pos + b_thickness );
1164
1165 aThickness = ( top - bottom );
1166 aZPos = bottom;
1167
1168 wxASSERT( aZPos == 0.0 );
1169}
1170
1171
1173 const VECTOR2D& aOrigin, const wxString& aNetname )
1174{
1175 bool success = true;
1176
1177 if( aPolyShapes->IsEmpty() )
1178 return true;
1179
1180 if( !m_enabledLayers.Contains( aLayer ) )
1181 return true;
1182
1183 double z_pos, thickness;
1184 getLayerZPlacement( aLayer, z_pos, thickness );
1185
1186 std::vector<TopoDS_Shape>& targetVec = IsCopperLayer( aLayer ) ? m_board_copper[aNetname]
1187 : aLayer == F_SilkS || aLayer == B_SilkS
1190
1191 if( !MakeShapes( targetVec, *aPolyShapes, m_simplifyShapes, thickness, z_pos, aOrigin ) )
1192 {
1193 m_reporter->Report( wxString::Format( _( "Could not add shape (%d points) to copper layer %s.\n" ),
1194 aPolyShapes->FullPointCount(),
1195 LayerName( aLayer ) ),
1197
1198 success = false;
1199 }
1200
1201 return success;
1202}
1203
1204
1205bool STEP_PCB_MODEL::AddComponent( const std::string& aFileNameUTF8, const std::string& aRefDes,
1206 bool aBottom, VECTOR2D aPosition, double aRotation, VECTOR3D aOffset,
1207 VECTOR3D aOrientation, VECTOR3D aScale, bool aSubstituteModels )
1208{
1209 if( aFileNameUTF8.empty() )
1210 {
1211 m_reporter->Report( wxString::Format( _( "No model defined for %s.\n" ), aRefDes ),
1213 return false;
1214 }
1215
1216 wxString fileName( wxString::FromUTF8( aFileNameUTF8.c_str() ) );
1217 m_reporter->Report( wxString::Format( wxT( "Adding component %s.\n" ), aRefDes ), RPT_SEVERITY_DEBUG );
1218
1219 // first retrieve a label
1220 TDF_Label lmodel;
1221 wxString errorMessage;
1222
1223 if( !getModelLabel( aFileNameUTF8, aScale, lmodel, aSubstituteModels, &errorMessage ) )
1224 {
1225 if( errorMessage.IsEmpty() )
1226 errorMessage.Printf( _( "No model for filename '%s'.\n" ), fileName );
1227
1228 m_reporter->Report( errorMessage, RPT_SEVERITY_ERROR );
1229 return false;
1230 }
1231
1232 // calculate the Location transform
1233 TopLoc_Location toploc;
1234
1235 if( !getModelLocation( aBottom, aPosition, aRotation, aOffset, aOrientation, toploc ) )
1236 {
1237 m_reporter->Report( wxString::Format( _( "No location data for filename '%s'.\n" ), fileName ),
1239 return false;
1240 }
1241
1242 // add the located sub-assembly
1243 TDF_Label llabel = m_assy->AddComponent( m_assy_label, lmodel, toploc );
1244
1245 if( llabel.IsNull() )
1246 {
1247 m_reporter->Report( wxString::Format( _( "Could not add component with filename '%s'.\n" ), fileName ),
1249 return false;
1250 }
1251
1252 // attach the RefDes name
1253 TCollection_ExtendedString refdes( aRefDes.c_str() );
1254 TDataStd_Name::Set( llabel, refdes );
1255
1256 return true;
1257}
1258
1259
1261{
1262 m_enabledLayers = aLayers;
1263}
1264
1265
1267{
1268 m_fuseShapes = aValue;
1269}
1270
1271
1273{
1274 m_simplifyShapes = aValue;
1275}
1276
1277
1279{
1280 m_stackup = aStackup;
1281}
1282
1283
1284void STEP_PCB_MODEL::SetNetFilter( const wxString& aFilter )
1285{
1286 m_netFilter = aFilter;
1287}
1288
1289
1290void STEP_PCB_MODEL::SetCopperColor( double r, double g, double b )
1291{
1292 m_copperColor[0] = r;
1293 m_copperColor[1] = g;
1294 m_copperColor[2] = b;
1295}
1296
1297
1298void STEP_PCB_MODEL::SetPadColor( double r, double g, double b )
1299{
1300 m_padColor[0] = r;
1301 m_padColor[1] = g;
1302 m_padColor[2] = b;
1303}
1304
1305
1307{
1308 // Ensure a minimal value (in mm)
1309 m_mergeOCCMaxDist = aDistance;
1310}
1311
1312
1314{
1315 return m_pcb_labels.size() > 0;
1316}
1317
1318
1320 VECTOR2D aStartPoint, VECTOR2D aEndPoint,
1321 double aWidth, double aThickness,
1322 double aZposition, const VECTOR2D& aOrigin )
1323{
1324 // make a wide segment from 2 lines and 2 180 deg arcs
1325 // We need 6 points (3 per arcs)
1326 VECTOR2D coords[6];
1327
1328 // We build a horizontal segment, and after rotate it
1329 double len = ( aEndPoint - aStartPoint ).EuclideanNorm();
1330 double h_width = aWidth/2.0;
1331 // First is end point of first arc, and also start point of first line
1332 coords[0] = VECTOR2D{ 0.0, h_width };
1333
1334 // end point of first line and start point of second arc
1335 coords[1] = VECTOR2D{ len, h_width };
1336
1337 // middle point of second arc
1338 coords[2] = VECTOR2D{ len + h_width, 0.0 };
1339
1340 // start point of second line and end point of second arc
1341 coords[3] = VECTOR2D{ len, -h_width };
1342
1343 // end point of second line and start point of first arc
1344 coords[4] = VECTOR2D{ 0, -h_width };
1345
1346 // middle point of first arc
1347 coords[5] = VECTOR2D{ -h_width, 0.0 };
1348
1349 // Rotate and move to segment position
1350 EDA_ANGLE seg_angle( aEndPoint - aStartPoint );
1351
1352 for( int ii = 0; ii < 6; ii++ )
1353 {
1354 RotatePoint( coords[ii], VECTOR2D{ 0, 0 }, -seg_angle ),
1355 coords[ii] += aStartPoint;
1356 }
1357
1358
1359 // Convert to 3D points
1360 gp_Pnt coords3D[ 6 ];
1361
1362 for( int ii = 0; ii < 6; ii++ )
1363 {
1364 coords3D[ii] = gp_Pnt( pcbIUScale.IUTomm( coords[ii].x - aOrigin.x ),
1365 -pcbIUScale.IUTomm( coords[ii].y - aOrigin.y ), aZposition );
1366 }
1367
1368 // Build OpenCascade shape outlines
1369 BRepBuilderAPI_MakeWire wire;
1370 bool success = true;
1371
1372 // Short segments (distance between end points < m_mergeOCCMaxDist(in mm)) must be
1373 // skipped because OCC merge end points, and a null shape is created
1374 bool short_seg = pcbIUScale.IUTomm( len ) <= m_mergeOCCMaxDist;
1375
1376 try
1377 {
1378 TopoDS_Edge edge;
1379
1380 if( short_seg )
1381 {
1382 Handle( Geom_Circle ) circle = GC_MakeCircle( coords3D[1], // arc1 start point
1383 coords3D[2], // arc1 mid point
1384 coords3D[5] // arc2 mid point
1385 );
1386
1387 edge = BRepBuilderAPI_MakeEdge( circle );
1388 wire.Add( edge );
1389 }
1390 else
1391 {
1392 edge = BRepBuilderAPI_MakeEdge( coords3D[0], coords3D[1] );
1393 wire.Add( edge );
1394
1395 Handle( Geom_TrimmedCurve ) arcOfCircle =
1396 GC_MakeArcOfCircle( coords3D[1], // start point
1397 coords3D[2], // mid point
1398 coords3D[3] // end point
1399 );
1400 edge = BRepBuilderAPI_MakeEdge( arcOfCircle );
1401 wire.Add( edge );
1402
1403 edge = BRepBuilderAPI_MakeEdge( coords3D[3], coords3D[4] );
1404 wire.Add( edge );
1405
1406 Handle( Geom_TrimmedCurve ) arcOfCircle2 =
1407 GC_MakeArcOfCircle( coords3D[4], // start point
1408 coords3D[5], // mid point
1409 coords3D[0] // end point
1410 );
1411 edge = BRepBuilderAPI_MakeEdge( arcOfCircle2 );
1412 wire.Add( edge );
1413 }
1414 }
1415 catch( const Standard_Failure& e )
1416 {
1417 m_reporter->Report( wxString::Format( _( "OCC exception building shape segment: %s\n" ),
1418 e.GetMessageString() ),
1420 return false;
1421 }
1422
1423 BRepBuilderAPI_MakeFace face;
1424
1425 try
1426 {
1427 gp_Pln plane( coords3D[0], gp::DZ() );
1428 face = BRepBuilderAPI_MakeFace( plane, wire );
1429 }
1430 catch( const Standard_Failure& e )
1431 {
1432 m_reporter->Report( wxString::Format( _( "OCC exception building face: %s\n" ),
1433 e.GetMessageString() ),
1435 return false;
1436 }
1437
1438 if( aThickness != 0.0 )
1439 {
1440 aShape = BRepPrimAPI_MakePrism( face, gp_Vec( 0, 0, aThickness ) );
1441
1442 if( aShape.IsNull() )
1443 {
1444 m_reporter->Report( _( "Failed to create a prismatic shape\n" ),
1446 return false;
1447 }
1448 }
1449 else
1450 {
1451 aShape = face;
1452 }
1453
1454 return success;
1455}
1456
1457
1458bool STEP_PCB_MODEL::MakePolygonAsWall( TopoDS_Shape& aShape,
1459 SHAPE_POLY_SET& aPolySet,
1460 double aHeight,
1461 double aZposition, const VECTOR2D& aOrigin )
1462{
1463 std::vector<TopoDS_Shape> testShapes;
1464
1465 bool success = MakeShapes( testShapes, aPolySet, m_simplifyShapes,
1466 aHeight, aZposition, aOrigin );
1467
1468 if( testShapes.size() > 0 )
1469 aShape = testShapes.front();
1470 else
1471 success = false;
1472
1473 return success;
1474}
1475
1476
1477static wxString formatBBox( const BOX2I& aBBox )
1478{
1479 wxString str;
1480 UNITS_PROVIDER unitsProvider( pcbIUScale, EDA_UNITS::MM );
1481
1482 str << "x0: " << unitsProvider.StringFromValue( aBBox.GetLeft(), false ) << "; ";
1483 str << "y0: " << unitsProvider.StringFromValue( aBBox.GetTop(), false ) << "; ";
1484 str << "x1: " << unitsProvider.StringFromValue( aBBox.GetRight(), false ) << "; ";
1485 str << "y1: " << unitsProvider.StringFromValue( aBBox.GetBottom(), false );
1486
1487 return str;
1488}
1489
1490
1491static bool makeWireFromChain( BRepLib_MakeWire& aMkWire, const SHAPE_LINE_CHAIN& aChain,
1492 double aMergeOCCMaxDist, double aZposition, const VECTOR2D& aOrigin,
1493 REPORTER* aReporter )
1494{
1495 auto toPoint =
1496 [&]( const VECTOR2D& aKiCoords ) -> gp_Pnt
1497 {
1498 return gp_Pnt( pcbIUScale.IUTomm( aKiCoords.x - aOrigin.x ),
1499 -pcbIUScale.IUTomm( aKiCoords.y - aOrigin.y ), aZposition );
1500 };
1501
1502 try
1503 {
1504 auto addSegment = [&]( const VECTOR2I& aPt0, const VECTOR2I& aPt1 ) -> bool
1505 {
1506 if( aPt0 == aPt1 )
1507 return false;
1508
1509 gp_Pnt start = toPoint( aPt0 );
1510 gp_Pnt end = toPoint( aPt1 );
1511
1512 BRepBuilderAPI_MakeEdge mkEdge( start, end );
1513
1514 if( !mkEdge.IsDone() || mkEdge.Edge().IsNull() )
1515 {
1516 aReporter->Report( wxString::Format( _( "Failed to make segment edge (%d %d) -> (%d %d), "
1517 "skipping\n" ),
1518 aPt0.x, aPt0.y,
1519 aPt1.x, aPt1.y ),
1521 }
1522 else
1523 {
1524 aMkWire.Add( mkEdge.Edge() );
1525
1526 if( aMkWire.Error() != BRepLib_WireDone )
1527 {
1528 aReporter->Report( wxString::Format( _( "Failed to add segment edge (%d %d) -> (%d %d)\n" ),
1529 aPt0.x, aPt0.y,
1530 aPt1.x, aPt1.y ),
1532 return false;
1533 }
1534 }
1535
1536 return true;
1537 };
1538
1539 auto addArc = [&]( const VECTOR2I& aPt0, const SHAPE_ARC& aArc ) -> bool
1540 {
1541 // Do not export too short segments: they create broken shape because OCC thinks
1542 Handle( Geom_Curve ) curve;
1543
1544 if( aArc.GetCentralAngle() == ANGLE_360 )
1545 {
1546 gp_Ax2 axis = gp::XOY();
1547 axis.SetLocation( toPoint( aArc.GetCenter() ) );
1548
1549 curve = GC_MakeCircle( axis, pcbIUScale.IUTomm( aArc.GetRadius() ) ).Value();
1550 }
1551 else
1552 {
1553 curve = GC_MakeArcOfCircle( toPoint( aPt0 ), toPoint( aArc.GetArcMid() ),
1554 toPoint( aArc.GetP1() ) ).Value();
1555 }
1556
1557 if( curve.IsNull() )
1558 return false;
1559
1560 aMkWire.Add( BRepBuilderAPI_MakeEdge( curve ) );
1561
1562 if( !aMkWire.IsDone() )
1563 {
1564 aReporter->Report( wxString::Format( _( "Failed to add arc curve from (%d %d), arc p0 "
1565 "(%d %d), mid (%d %d), p1 (%d %d)\n" ),
1566 aPt0.x, aPt0.y,
1567 aArc.GetP0().x, aArc.GetP0().y,
1568 aArc.GetArcMid().x, aArc.GetArcMid().y,
1569 aArc.GetP1().x, aArc.GetP1().y ),
1571 return false;
1572 }
1573
1574 return true;
1575 };
1576
1577 VECTOR2I firstPt;
1578 VECTOR2I lastPt;
1579 bool isFirstShape = true;
1580
1581 for( int i = 0; i <= aChain.PointCount() && i != -1; i = aChain.NextShape( i ) )
1582 {
1583 if( i == 0 )
1584 {
1585 if( aChain.IsArcSegment( 0 ) && aChain.IsArcSegment( aChain.PointCount() - 1 )
1586 && aChain.ArcIndex( 0 ) == aChain.ArcIndex( aChain.PointCount() - 1 ) )
1587 {
1588 // Skip first arc (we should encounter it later)
1589 int nextShape = aChain.NextShape( i );
1590
1591 // If nextShape points to the end, then we have a circle.
1592 if( nextShape != -1 )
1593 i = nextShape;
1594 }
1595 }
1596
1597 if( isFirstShape )
1598 lastPt = aChain.CPoint( i );
1599
1600 bool isArc = aChain.IsArcSegment( i );
1601
1602 if( aChain.IsArcStart( i ) )
1603 {
1604 const SHAPE_ARC& currentArc = aChain.Arc( aChain.ArcIndex( i ) );
1605
1606 if( isFirstShape )
1607 {
1608 firstPt = currentArc.GetP0();
1609 lastPt = firstPt;
1610 }
1611
1612 if( addSegment( lastPt, currentArc.GetP0() ) )
1613 lastPt = currentArc.GetP0();
1614
1615 if( addArc( lastPt, currentArc ) )
1616 lastPt = currentArc.GetP1();
1617 }
1618 else if( !isArc )
1619 {
1620 const SEG& seg = aChain.CSegment( i );
1621
1622 if( isFirstShape )
1623 {
1624 firstPt = seg.A;
1625 lastPt = firstPt;
1626 }
1627
1628 if( addSegment( lastPt, seg.A ) )
1629 lastPt = seg.A;
1630
1631 if( addSegment( lastPt, seg.B ) )
1632 lastPt = seg.B;
1633 }
1634
1635 isFirstShape = false;
1636 }
1637
1638 if( lastPt != firstPt && !addSegment( lastPt, firstPt ) )
1639 {
1640 aReporter->Report( wxString::Format( _( "Failed to close wire at %d, %d -> %d, %d **\n" ),
1641 lastPt.x, lastPt.y,
1642 firstPt.x, firstPt.y ),
1644
1645 return false;
1646 }
1647 }
1648 catch( const Standard_Failure& e )
1649 {
1650 aReporter->Report( wxString::Format( _( "OCC exception creating wire: %s\n" ),
1651 e.GetMessageString() ),
1653 return false;
1654 }
1655
1656 return true;
1657}
1658
1659
1660bool STEP_PCB_MODEL::MakeShapes( std::vector<TopoDS_Shape>& aShapes, const SHAPE_POLY_SET& aPolySet,
1661 bool aConvertToArcs, double aThickness, double aZposition,
1662 const VECTOR2D& aOrigin )
1663{
1664 SHAPE_POLY_SET workingPoly = aPolySet;
1665 workingPoly.Simplify();
1666
1667 SHAPE_POLY_SET fallbackPoly = workingPoly;
1668
1669 if( aConvertToArcs )
1670 {
1671 SHAPE_POLY_SET approximated = workingPoly;
1672
1673 for( size_t polyId = 0; polyId < approximated.CPolygons().size(); polyId++ )
1674 {
1675 SHAPE_POLY_SET::POLYGON& polygon = approximated.Polygon( polyId );
1676
1677 for( size_t contId = 0; contId < polygon.size(); contId++ )
1678 polygon[contId] = approximateLineChainWithArcs( polygon[contId] );
1679 }
1680
1681 fallbackPoly = workingPoly;
1682 workingPoly = approximated;
1683
1684 // TODO: this is not accurate because it doesn't check arcs.
1685 /*if( approximated.IsSelfIntersecting() )
1686 {
1687 m_reporter->Report( wxString::Format( _( "Approximated polygon self-intersection check failed\n"
1688 "z: %g; bounding box: %s\n" ) ),
1689 aZposition,
1690 formatBBox( workingPoly.BBox() ) ),
1691 RPT_SEVERITY_ERROR );
1692 }
1693 else
1694 {
1695 fallbackPoly = workingPoly;
1696 workingPoly = approximated;
1697 }*/
1698 }
1699
1700#if 0 // No longer in use
1701 auto toPoint = [&]( const VECTOR2D& aKiCoords ) -> gp_Pnt
1702 {
1703 return gp_Pnt( pcbIUScale.IUTomm( aKiCoords.x - aOrigin.x ),
1704 -pcbIUScale.IUTomm( aKiCoords.y - aOrigin.y ), aZposition );
1705 };
1706#endif
1707
1708 gp_Pln basePlane( gp_Pnt( 0.0, 0.0, aZposition ),
1709 std::signbit( aThickness ) ? -gp::DZ() : gp::DZ() );
1710
1711 for( size_t polyId = 0; polyId < workingPoly.CPolygons().size(); polyId++ )
1712 {
1713 SHAPE_POLY_SET::POLYGON& polygon = workingPoly.Polygon( polyId );
1714
1715 auto tryMakeWire = [this, &aZposition,
1716 &aOrigin]( const SHAPE_LINE_CHAIN& aContour ) -> TopoDS_Wire
1717 {
1718 TopoDS_Wire wire;
1719 BRepLib_MakeWire mkWire;
1720
1721 makeWireFromChain( mkWire, aContour, m_mergeOCCMaxDist, aZposition, aOrigin, m_reporter );
1722
1723 if( mkWire.IsDone() )
1724 {
1725 wire = mkWire.Wire();
1726 }
1727 else
1728 {
1729 m_reporter->Report( wxString::Format( _( "Wire not done (contour points %d): OCC error %d\n"
1730 "z: %g; bounding box: %s\n" ),
1731 static_cast<int>( aContour.PointCount() ),
1732 static_cast<int>( mkWire.Error() ),
1733 formatBBox( aContour.BBox() ) ),
1735 }
1736
1737 if( !wire.IsNull() )
1738 {
1739 BRepAlgoAPI_Check check( wire, false, true );
1740
1741 if( !check.IsValid() )
1742 {
1743 m_reporter->Report( wxString::Format( _( "Wire self-interference check failed\n"
1744 "z: %g; bounding box: %s\n" ),
1745 aZposition,
1746 formatBBox( aContour.BBox() ) ),
1748 wire.Nullify();
1749 }
1750 }
1751
1752 return wire;
1753 };
1754
1755 BRepBuilderAPI_MakeFace mkFace;
1756
1757 for( size_t contId = 0; contId < polygon.size(); contId++ )
1758 {
1759 try
1760 {
1761 TopoDS_Wire wire = tryMakeWire( polygon[contId] );
1762
1763 if( aConvertToArcs && wire.IsNull() )
1764 {
1765 m_reporter->Report( wxString::Format( _( "Using non-simplified polygon.\n" ) ),
1767
1768 // Fall back to original shape
1769 wire = tryMakeWire( fallbackPoly.CPolygon( polyId )[contId] );
1770 }
1771
1772 if( contId == 0 ) // Outline
1773 {
1774 if( !wire.IsNull() )
1775 {
1776 if( basePlane.Axis().Direction().Z() < 0 )
1777 wire.Reverse();
1778
1779 mkFace = BRepBuilderAPI_MakeFace( basePlane, wire );
1780 }
1781 else
1782 {
1783 m_reporter->Report( wxString::Format( wxT( "** Outline skipped **\n"
1784 "z: %g; bounding box: %s\n" ),
1785 aZposition,
1786 formatBBox( polygon[contId].BBox() ) ),
1788 break;
1789 }
1790 }
1791 else // Hole
1792 {
1793 if( !wire.IsNull() )
1794 {
1795 if( basePlane.Axis().Direction().Z() > 0 )
1796 wire.Reverse();
1797
1798 mkFace.Add( wire );
1799 }
1800 else
1801 {
1802 m_reporter->Report( wxString::Format( wxT( "** Hole skipped **\n"
1803 "z: %g; bounding box: %s\n" ),
1804 aZposition,
1805 formatBBox( polygon[contId].BBox() ) ),
1807 }
1808 }
1809 }
1810 catch( const Standard_Failure& e )
1811 {
1812 m_reporter->Report( wxString::Format( _( "OCC exception creating contour %d: %s\n" ),
1813 static_cast<int>( contId ),
1814 e.GetMessageString() ),
1816 return false;
1817 }
1818 }
1819
1820 if( mkFace.IsDone() )
1821 {
1822 TopoDS_Shape faceShape = mkFace.Shape();
1823
1824 if( aThickness != 0.0 )
1825 {
1826 TopoDS_Shape prism = BRepPrimAPI_MakePrism( faceShape, gp_Vec( 0, 0, aThickness ) );
1827 aShapes.push_back( prism );
1828
1829 if( prism.IsNull() )
1830 {
1831 m_reporter->Report( _( "Failed to create a prismatic shape\n" ), RPT_SEVERITY_ERROR );
1832 return false;
1833 }
1834 }
1835 else
1836 {
1837 aShapes.push_back( faceShape );
1838 }
1839 }
1840 else
1841 {
1842 m_reporter->Report( _( "** Face skipped **\n" ), RPT_SEVERITY_DEBUG );
1843 }
1844 }
1845
1846 return true;
1847}
1848
1849
1850// These colors are based on 3D viewer's colors and are different to "gbrjobColors"
1851static std::vector<FAB_LAYER_COLOR> s_soldermaskColors = {
1852 { NotSpecifiedPrm(), wxColor( 20, 51, 36 ) }, // Not specified, not in .gbrjob file
1853 { _HKI( "Green" ), wxColor( 20, 51, 36 ) }, // used in .gbrjob file
1854 { _HKI( "Red" ), wxColor( 181, 19, 21 ) }, // used in .gbrjob file
1855 { _HKI( "Blue" ), wxColor( 2, 59, 162 ) }, // used in .gbrjob file
1856 { _HKI( "Purple" ), wxColor( 32, 2, 53 ) }, // used in .gbrjob file
1857 { _HKI( "Black" ), wxColor( 11, 11, 11 ) }, // used in .gbrjob file
1858 { _HKI( "White" ), wxColor( 245, 245, 245 ) }, // used in .gbrjob file
1859 { _HKI( "Yellow" ), wxColor( 194, 195, 0 ) }, // used in .gbrjob file
1860 { _HKI( "User defined" ), wxColor( 128, 128, 128 ) } // Free; the name is a dummy name here
1861};
1862
1863
1864static bool colorFromStackup( BOARD_STACKUP_ITEM_TYPE aType, const wxString& aColorStr,
1865 COLOR4D& aColorOut )
1866{
1867 if( !IsPrmSpecified( aColorStr ) )
1868 return false;
1869
1870 if( aColorStr.StartsWith( wxT( "#" ) ) ) // User defined color
1871 {
1872 aColorOut = COLOR4D( aColorStr );
1873 return true;
1874 }
1875 else
1876 {
1877 const std::vector<FAB_LAYER_COLOR>& colors =
1878 ( aType == BS_ITEM_TYPE_SOLDERMASK || aType == BS_ITEM_TYPE_SILKSCREEN )
1880 : GetStandardColors( aType );
1881
1882 for( const FAB_LAYER_COLOR& fabColor : colors )
1883 {
1884 if( fabColor.GetName() == aColorStr )
1885 {
1886 aColorOut = fabColor.GetColor( aType );
1887 return true;
1888 }
1889 }
1890 }
1891
1892 return false;
1893}
1894
1895
1896bool STEP_PCB_MODEL::CreatePCB( SHAPE_POLY_SET& aOutline, VECTOR2D aOrigin, bool aPushBoardBody )
1897{
1898 if( m_hasPCB )
1899 {
1900 if( !isBoardOutlineValid() )
1901 return false;
1902
1903 return true;
1904 }
1905
1907
1908 Handle( XCAFDoc_VisMaterialTool ) visMatTool = XCAFDoc_DocumentTool::VisMaterialTool( m_doc->Main() );
1909
1910 m_hasPCB = true; // whether or not operations fail we note that CreatePCB has been invoked
1911
1912 // Support for more than one main outline (more than one board)
1913 m_reporter->Report( wxString::Format( wxT( "Build board outlines (%d outlines) with %d points.\n" ),
1914 aOutline.OutlineCount(),
1915 aOutline.FullPointCount() ),
1917
1918 double boardThickness;
1919 double boardZPos;
1920 getBoardBodyZPlacement( boardZPos, boardThickness );
1921
1922#if 1
1923 // This code should work, and it is working most of time
1924 // However there are issues if the main outline is a circle with holes:
1925 // holes from vias and pads are not working
1926 // see bug https://gitlab.com/kicad/code/kicad/-/issues/17446
1927 // (Holes are missing from STEP export with circular PCB outline)
1928 // Hard to say if the bug is in our code or in OCC 7.7
1929 if( !MakeShapes( m_board_outlines, aOutline, false, boardThickness, boardZPos, aOrigin ) )
1930 {
1931 // Error
1932 m_reporter->Report( _( "OCC error creating main outline.\n" ), RPT_SEVERITY_ERROR );
1933 }
1934#else
1935 // Workaround for bug #17446 Holes are missing from STEP export with circular PCB outline
1936 for( const SHAPE_POLY_SET::POLYGON& polygon : aOutline.CPolygons() )
1937 {
1938 for( size_t contId = 0; contId < polygon.size(); contId++ )
1939 {
1940 const SHAPE_LINE_CHAIN& contour = polygon[contId];
1941 SHAPE_POLY_SET polyset;
1942 polyset.Append( contour );
1943
1944 if( contId == 0 ) // main Outline
1945 {
1946 if( !MakeShapes( m_board_outlines, polyset, false, boardThickness, boardZPos,
1947 aOrigin ) )
1948 {
1949 m_reporter->Report( _( "OCC error creating main outline.\n" ),
1951 }
1952 }
1953 else // Hole inside the main outline
1954 {
1955 if( !MakeShapes( m_boardCutouts, polyset, false, boardThickness, boardZPos,
1956 aOrigin ) )
1957 {
1958 m_reporter->Report( _( "OCC error creating hole in main outline.\n" ),
1960 }
1961 }
1962 }
1963 }
1964#endif
1965
1966 // Even if we've disabled board body export, we still need the shapes for bounding box calculations.
1967 Bnd_Box brdBndBox;
1968
1969 for( const TopoDS_Shape& brdShape : m_board_outlines )
1970 BRepBndLib::Add( brdShape, brdBndBox );
1971
1972 // subtract cutouts (if any)
1973 m_reporter->Report( wxString::Format( wxT( "Build board cutouts and holes (%d hole(s)).\n" ),
1974 (int) ( m_boardCutouts.size() + m_copperCutouts.size() ) ),
1976
1977 auto buildBSB =
1978 [&brdBndBox]( std::vector<TopoDS_Shape>& input, Bnd_BoundSortBox& bsbHoles )
1979 {
1980 // We need to encompass every location we'll need to test in the global bbox,
1981 // otherwise Bnd_BoundSortBox doesn't work near the boundaries.
1982 Bnd_Box brdWithHolesBndBox = brdBndBox;
1983
1984 Handle( Bnd_HArray1OfBox ) holeBoxSet = new Bnd_HArray1OfBox( 0, input.size() - 1 );
1985
1986 for( size_t i = 0; i < input.size(); i++ )
1987 {
1988 Bnd_Box bbox;
1989 BRepBndLib::Add( input[i], bbox );
1990 brdWithHolesBndBox.Add( bbox );
1991 ( *holeBoxSet )[i] = bbox;
1992 }
1993
1994 bsbHoles.Initialize( brdWithHolesBndBox, holeBoxSet );
1995 };
1996
1997 auto subtractShapesMap =
1998 [&tp, this]( const wxString& aWhat, std::map<wxString, std::vector<TopoDS_Shape>>& aShapesMap,
1999 std::vector<TopoDS_Shape>& aHolesList, Bnd_BoundSortBox& aBSBHoles )
2000 {
2001 m_reporter->Report( wxString::Format( _( "Subtracting holes for %s\n" ), aWhat ),
2003
2004 for( auto& [netname, vec] : aShapesMap )
2005 {
2006 std::mutex mutex;
2007
2008 auto subtractLoopFn = [&]( const int a, const int b )
2009 {
2010 for( int shapeId = a; shapeId < b; shapeId++ )
2011 {
2012 TopoDS_Shape& shape = vec[shapeId];
2013
2014 Bnd_Box shapeBbox;
2015 BRepBndLib::Add( shape, shapeBbox );
2016
2017 TopTools_ListOfShape holelist;
2018
2019 {
2020 std::unique_lock lock( mutex );
2021
2022 const TColStd_ListOfInteger& indices = aBSBHoles.Compare( shapeBbox );
2023
2024 for( const Standard_Integer& index : indices )
2025 holelist.Append( aHolesList[index] );
2026 }
2027
2028 if( holelist.IsEmpty() )
2029 continue;
2030
2031 TopTools_ListOfShape cutArgs;
2032 cutArgs.Append( shape );
2033
2034 BRepAlgoAPI_Cut cut;
2035
2036 cut.SetRunParallel( true );
2037 cut.SetToFillHistory( false );
2038
2039 cut.SetArguments( cutArgs );
2040 cut.SetTools( holelist );
2041 cut.Build();
2042
2043 if( cut.HasErrors() || cut.HasWarnings() )
2044 {
2045 m_reporter->Report( wxString::Format( _( "** Got problems while cutting "
2046 "%s net '%s' **\n" ),
2047 aWhat,
2048 UnescapeString( netname ) ),
2050 shapeBbox.Dump();
2051
2052 if( cut.HasErrors() )
2053 {
2054 wxString msg = _( "Errors:\n" );
2055 wxStringOutputStream os_stream( &msg );
2056 wxStdOutputStream out( os_stream );
2057
2058 cut.DumpErrors( out );
2060 }
2061
2062 if( cut.HasWarnings() )
2063 {
2064 wxString msg = _( "Warnings:\n" );
2065 wxStringOutputStream os_stream( &msg );
2066 wxStdOutputStream out( os_stream );
2067
2068 cut.DumpWarnings( out );
2070 }
2071 }
2072
2073 shape = cut.Shape();
2074 }
2075 };
2076
2077 tp.parallelize_loop( vec.size(), subtractLoopFn ).wait();
2078 }
2079 };
2080
2081 auto subtractShapes =
2082 [subtractShapesMap]( const wxString& aWhat, std::vector<TopoDS_Shape>& aShapesList,
2083 std::vector<TopoDS_Shape>& aHolesList, Bnd_BoundSortBox& aBSBHoles )
2084 {
2085 std::map<wxString, std::vector<TopoDS_Shape>> aShapesMap{ { wxEmptyString, aShapesList } };
2086
2087 subtractShapesMap( aWhat, aShapesMap, aHolesList, aBSBHoles );
2088 aShapesList = aShapesMap[wxEmptyString];
2089 };
2090
2091
2092 if( m_boardCutouts.size() )
2093 {
2094 Bnd_BoundSortBox bsbHoles;
2095 buildBSB( m_boardCutouts, bsbHoles );
2096
2097 subtractShapes( _( "shapes" ), m_board_outlines, m_boardCutouts, bsbHoles );
2098 }
2099
2100 if( m_copperCutouts.size() )
2101 {
2102 Bnd_BoundSortBox bsbHoles;
2103 buildBSB( m_copperCutouts, bsbHoles );
2104
2105 subtractShapesMap( _( "pads" ), m_board_copper_pads, m_copperCutouts, bsbHoles );
2106 subtractShapesMap( _( "vias" ), m_board_copper_vias, m_copperCutouts, bsbHoles );
2107 }
2108
2109 if( m_fuseShapes )
2110 {
2111 std::map<wxString, TopTools_ListOfShape> shapesToFuseMap;
2112
2113 auto addShapes = [&shapesToFuseMap]( const wxString& aNetname,
2114 const std::vector<TopoDS_Shape>& aShapes )
2115 {
2116 for( const TopoDS_Shape& shape : aShapes )
2117 shapesToFuseMap[aNetname].Append( shape );
2118 };
2119
2120 for( const auto& [netname, shapes] : m_board_copper )
2121 addShapes( netname, shapes );
2122
2123 for( const auto& [netname, shapes] : m_board_copper_pads )
2124 addShapes( netname, shapes );
2125
2126 for( const auto& [netname, shapes] : m_board_copper_vias )
2127 addShapes( netname, shapes );
2128
2129 m_reporter->Report( wxT( "Fusing shapes\n" ), RPT_SEVERITY_DEBUG );
2130
2131 // Do fusing in parallel
2132 std::mutex mutex;
2133
2134 auto fuseLoopFn = [&]( const wxString& aNetname )
2135 {
2136 auto& toFuse = shapesToFuseMap[aNetname];
2137 TopoDS_Shape fusedShape = fuseShapesOrCompound( toFuse, m_reporter );
2138
2139 if( !fusedShape.IsNull() )
2140 {
2141 std::unique_lock lock( mutex );
2142
2143 m_board_copper_fused[aNetname].emplace_back( fusedShape );
2144
2145 m_board_copper[aNetname].clear();
2146 m_board_copper_pads[aNetname].clear();
2147 m_board_copper_vias[aNetname].clear();
2148 }
2149 };
2150
2151 BS::multi_future<void> mf;
2152
2153 for( const auto& [netname, _] : shapesToFuseMap )
2154 mf.push_back( tp.submit( fuseLoopFn, netname ) );
2155
2156 mf.wait();
2157 }
2158
2159 // push the board to the data structure
2160 m_reporter->Report( wxT( "Generate board full shape.\n" ), RPT_SEVERITY_DEBUG );
2161
2162 // AddComponent adds a label that has a reference (not a parent/child relation) to the real
2163 // label. We need to extract that real label to name it for the STEP output cleanly
2164 // Why are we trying to name the bare board? Because CAD tools like SolidWorks do fun things
2165 // like "deduplicate" imported STEPs by swapping STEP assembly components with already
2166 // identically named assemblies. So we want to avoid having the PCB be generally defaulted
2167 // to "Component" or "Assembly".
2168
2169 // aCompoundNets will place all geometry within a net into one compound.
2170 // aCompoundAll will place all geometry into one compound.
2171 auto pushToAssemblyMap =
2172 [&]( const std::map<wxString, std::vector<TopoDS_Shape>>& aShapesMap,
2173 const TDF_Label& aVisMatLabel, const wxString& aShapeName, bool aCompoundNets,
2174 bool aCompoundAll )
2175 {
2176 std::map<wxString, std::vector<TopoDS_Shape>> shapesMap;
2177
2178 if( aCompoundAll )
2179 {
2180 std::vector<TopoDS_Shape> allShapes;
2181
2182 for( const auto& [netname, shapesList] : aShapesMap )
2183 allShapes.insert( allShapes.end(), shapesList.begin(), shapesList.end() );
2184
2185 if( !allShapes.empty() )
2186 shapesMap[wxEmptyString].emplace_back( makeCompound( allShapes ) );
2187 }
2188 else
2189 {
2190 shapesMap = aShapesMap;
2191 }
2192
2193 for( const auto& [netname, shapesList] : shapesMap )
2194 {
2195 std::vector<TopoDS_Shape> newList;
2196
2197 if( aCompoundNets )
2198 newList.emplace_back( makeCompound( shapesList ) );
2199 else
2200 newList = shapesList;
2201
2202 int i = 1;
2203
2204 for( TopoDS_Shape& shape : newList )
2205 {
2206 Handle( TDataStd_TreeNode ) node;
2207
2208 // Dont expand the component or else coloring it gets hard
2209 TDF_Label lbl = m_assy->AddComponent( m_assy_label, shape, false );
2210 m_pcb_labels.push_back( lbl );
2211
2212 if( m_pcb_labels.back().IsNull() )
2213 return;
2214
2215 lbl.FindAttribute( XCAFDoc::ShapeRefGUID(), node );
2216 TDF_Label shpLbl = node->Father()->Label();
2217
2218 if( !shpLbl.IsNull() )
2219 {
2220 if( visMatTool && !aVisMatLabel.IsNull() )
2221 visMatTool->SetShapeMaterial( shpLbl, aVisMatLabel );
2222
2223 wxString shapeName;
2224
2225 shapeName << m_pcbName;
2226 shapeName << '_';
2227 shapeName << aShapeName;
2228
2229 if( !netname.empty() )
2230 {
2231 shapeName << '_';
2232 shapeName << netname;
2233 }
2234
2235 if( newList.size() > 1 )
2236 {
2237 shapeName << '_';
2238 shapeName << i;
2239 }
2240
2241 TCollection_ExtendedString partname( shapeName.ToUTF8().data() );
2242 TDataStd_Name::Set( shpLbl, partname );
2243 }
2244
2245 i++;
2246 }
2247 }
2248 };
2249
2250 auto pushToAssembly =
2251 [&]( const std::vector<TopoDS_Shape>& aShapesList, const TDF_Label& aVisMatLabel,
2252 const wxString& aShapeName, bool aCompound )
2253 {
2254 const std::map<wxString, std::vector<TopoDS_Shape>> shapesMap{ { wxEmptyString, aShapesList } };
2255
2256 pushToAssemblyMap( shapesMap, aVisMatLabel, aShapeName, aCompound, aCompound );
2257 };
2258
2259 auto makeMaterial =
2260 [&]( const TCollection_AsciiString& aName, const Quantity_ColorRGBA& aBaseColor,
2261 double aMetallic, double aRoughness ) -> TDF_Label
2262 {
2263 Handle( XCAFDoc_VisMaterial ) vismat = new XCAFDoc_VisMaterial;
2264 XCAFDoc_VisMaterialPBR pbr;
2265 pbr.BaseColor = aBaseColor;
2266 pbr.Metallic = aMetallic;
2267 pbr.Roughness = aRoughness;
2268 vismat->SetPbrMaterial( pbr );
2269 return visMatTool->AddMaterial( vismat, aName );
2270 };
2271
2272 // Init colors for the board items
2273 Quantity_ColorRGBA copper_color( m_copperColor[0], m_copperColor[1], m_copperColor[2], 1.0 );
2274 Quantity_ColorRGBA pad_color( m_padColor[0], m_padColor[1], m_padColor[2], 1.0 );
2275
2276 Quantity_ColorRGBA board_color( 0.3f, 0.3f, 0.3f, 1.0f );
2277 Quantity_ColorRGBA silk_color( 1.0f, 1.0f, 1.0f, 0.9f );
2278 Quantity_ColorRGBA mask_color( 0.08f, 0.2f, 0.14f, 0.83f );
2279
2280 // Get colors from stackup
2281 for( const BOARD_STACKUP_ITEM* item : m_stackup.GetList() )
2282 {
2283 COLOR4D col;
2284
2285 if( !colorFromStackup( item->GetType(), item->GetColor(), col ) )
2286 continue;
2287
2288 if( item->GetBrdLayerId() == F_Mask || item->GetBrdLayerId() == B_Mask )
2289 {
2290 col.Darken( 0.2 );
2291 mask_color.SetValues( col.r, col.g, col.b, col.a );
2292 }
2293
2294 if( item->GetBrdLayerId() == F_SilkS || item->GetBrdLayerId() == B_SilkS )
2295 silk_color.SetValues( col.r, col.g, col.b, col.a );
2296
2297 if( item->GetType() == BS_ITEM_TYPE_DIELECTRIC && item->GetTypeName() == KEY_CORE )
2298 board_color.SetValues( col.r, col.g, col.b, col.a );
2299 }
2300
2302 {
2303 board_color = mask_color;
2304 board_color.SetAlpha( 1.0 );
2305 }
2306
2307 TDF_Label mask_mat = makeMaterial( "soldermask", mask_color, 0.0, 0.6 );
2308 TDF_Label silk_mat = makeMaterial( "silkscreen", silk_color, 0.0, 0.9 );
2309 TDF_Label copper_mat = makeMaterial( "copper", copper_color, 1.0, 0.4 );
2310 TDF_Label pad_mat = makeMaterial( "pad", pad_color, 1.0, 0.4 );
2311 TDF_Label board_mat = makeMaterial( "board", board_color, 0.0, 0.8 );
2312
2313 pushToAssemblyMap( m_board_copper, copper_mat, "copper", true, true );
2314 pushToAssemblyMap( m_board_copper_pads, pad_mat, "pad", true, true );
2315 pushToAssemblyMap( m_board_copper_vias, copper_mat, "via", true, true );
2316 pushToAssemblyMap( m_board_copper_fused, copper_mat, "copper", true, true );
2317 pushToAssembly( m_board_silkscreen, silk_mat, "silkscreen", true );
2318 pushToAssembly( m_board_soldermask, mask_mat, "soldermask", true );
2319
2320 if( aPushBoardBody )
2321 pushToAssembly( m_board_outlines, board_mat, "PCB", false );
2322
2323#if( defined OCC_VERSION_HEX ) && ( OCC_VERSION_HEX > 0x070101 )
2324 m_assy->UpdateAssemblies();
2325#endif
2326
2327 return true;
2328}
2329
2330
2331#ifdef SUPPORTS_IGES
2332// write the assembly model in IGES format
2333bool STEP_PCB_MODEL::WriteIGES( const wxString& aFileName )
2334{
2335 if( !isBoardOutlineValid() )
2336 {
2337 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'.\n" ),
2338 aFileName ),
2340 return false;
2341 }
2342
2343 m_outFmt = OUTPUT_FORMAT::FMT_OUT_IGES;
2344
2345 wxFileName fn( aFileName );
2346 IGESControl_Controller::Init();
2347 IGESCAFControl_Writer writer;
2348 writer.SetColorMode( Standard_True );
2349 writer.SetNameMode( Standard_True );
2350 IGESData_GlobalSection header = writer.Model()->GlobalSection();
2351 header.SetFileName( new TCollection_HAsciiString( fn.GetFullName().ToAscii() ) );
2352 header.SetSendName( new TCollection_HAsciiString( "KiCad electronic assembly" ) );
2353 header.SetAuthorName( new TCollection_HAsciiString( Interface_Static::CVal( "write.iges.header.author" ) ) );
2354 header.SetCompanyName( new TCollection_HAsciiString( Interface_Static::CVal( "write.iges.header.company" ) ) );
2355 writer.Model()->SetGlobalSection( header );
2356
2357 if( Standard_False == writer.Perform( m_doc, aFileName.c_str() ) )
2358 return false;
2359
2360 return true;
2361}
2362#endif
2363
2364bool STEP_PCB_MODEL::CompressSTEP( wxString& inputFile, wxString& outputFile )
2365{
2366 wxFileInputStream input( inputFile );
2367 wxFileOutputStream output( outputFile );
2368
2369 if( !input.IsOk() )
2370 {
2371 m_reporter->Report( wxString::Format( _( "Cannot create input stream '%s'.\n" ), inputFile ) );
2372 return false;
2373 }
2374
2375 if( !output.IsOk() )
2376 {
2377 m_reporter->Report( wxString::Format( _( "Cannot create output stream '%s'.\n" ), outputFile ) );
2378 return false;
2379 }
2380
2381 wxZlibOutputStream zlibStream( output, -1, wxZLIB_GZIP );
2382
2383 if( !zlibStream.IsOk() )
2384 {
2385 m_reporter->Report( _( "Impossible create compress stream" ) );
2386 return false;
2387 }
2388
2389 input.Read( zlibStream );
2390
2391 if( input.LastRead() == 0 || zlibStream.LastWrite() == 0 )
2392 {
2393 m_reporter->Report( _( "Compress read or write error" ) );
2394 return false;
2395 }
2396
2397 zlibStream.Close();
2398 output.Close();
2399
2400 return true;
2401}
2402
2403bool STEP_PCB_MODEL::WriteSTEP( const wxString& aFileName, bool aOptimize, bool compress )
2404{
2405 if( !isBoardOutlineValid() )
2406 {
2407 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'.\n" ),
2408 aFileName ),
2410 return false;
2411 }
2412
2413 m_outFmt = OUTPUT_FORMAT::FMT_OUT_STEP;
2414
2415 wxFileName fn( aFileName );
2416
2417 STEPCAFControl_Writer writer;
2418 writer.SetColorMode( Standard_True );
2419 writer.SetNameMode( Standard_True );
2420
2421 // This must be set before we "transfer" the document.
2422 // Should default to kicad_pcb.general.title_block.title,
2423 // but in the meantime, defaulting to the basename of the output
2424 // target is still better than "open cascade step translter v..."
2425 // UTF8 should be ok from ISO 10303-21:2016, but... older stuff? use boring ascii
2426 if( !Interface_Static::SetCVal( "write.step.product.name", fn.GetName().ToAscii() ) )
2427 {
2428 m_reporter->Report( _( "Failed to set STEP product name, but will attempt to continue." ),
2430 }
2431
2432 // Setting write.surfacecurve.mode to 0 reduces file size and write/read times.
2433 // But there are reports that this mode might be less compatible in some cases.
2434 if( !Interface_Static::SetIVal( "write.surfacecurve.mode", aOptimize ? 0 : 1 ) )
2435 {
2436 m_reporter->Report( _( "Failed to set surface curve mode, but will attempt to continue." ),
2438 }
2439
2440 if( Standard_False == writer.Transfer( m_doc, STEPControl_AsIs ) )
2441 return false;
2442
2443 APIHeaderSection_MakeHeader hdr( writer.ChangeWriter().Model() );
2444
2445 // Note: use only Ascii7 chars, non Ascii7 chars (therefore UFT8 chars)
2446 // are creating issues in the step file
2447 hdr.SetName( new TCollection_HAsciiString( fn.GetFullName().ToAscii() ) );
2448
2449 // TODO: how to control and ensure consistency with IGES?
2450 hdr.SetAuthorValue( 1, new TCollection_HAsciiString( "Pcbnew" ) );
2451 hdr.SetOrganizationValue( 1, new TCollection_HAsciiString( "Kicad" ) );
2452 hdr.SetOriginatingSystem( new TCollection_HAsciiString( "KiCad to STEP converter" ) );
2453 hdr.SetDescriptionValue( 1, new TCollection_HAsciiString( "KiCad electronic assembly" ) );
2454
2455 bool success = true;
2456
2457 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
2458 wxString currCWD = wxGetCwd();
2459 wxString workCWD = fn.GetPath();
2460
2461 if( !workCWD.IsEmpty() )
2462 wxSetWorkingDirectory( workCWD );
2463
2464 wxString tmpfname( "$tempfile$.step" );
2465
2466 if( Standard_False == writer.Write( tmpfname.c_str() ) )
2467 success = false;
2468
2469 if( compress && success )
2470 {
2471 wxString srcTmp( tmpfname );
2472 wxString dstTmp( "$tempfile$.stpz" );
2473
2474 success = STEP_PCB_MODEL::CompressSTEP( srcTmp, dstTmp );
2475 wxRemoveFile( srcTmp );
2476
2477 tmpfname = dstTmp;
2478 }
2479
2480 if( success )
2481 {
2482
2483 // Preserve the permissions of the current file
2484 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpfname.c_str() );
2485
2486 if( !wxRenameFile( tmpfname, fn.GetFullName(), true ) )
2487 {
2488 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'.\n" ),
2489 tmpfname,
2490 fn.GetFullName() ),
2492 success = false;
2493 }
2494 }
2495
2496 wxSetWorkingDirectory( currCWD );
2497
2498 return success;
2499}
2500
2501
2502bool STEP_PCB_MODEL::WriteBREP( const wxString& aFileName )
2503{
2504 if( !isBoardOutlineValid() )
2505 {
2506 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'.\n" ),
2507 aFileName ),
2509 return false;
2510 }
2511
2512 m_outFmt = OUTPUT_FORMAT::FMT_OUT_BREP;
2513
2514 // s_assy = shape tool for the source
2515 Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
2516
2517 // retrieve assembly as a single shape
2518 TopoDS_Shape shape = getOneShape( s_assy );
2519
2520 wxFileName fn( aFileName );
2521
2522 wxFFileOutputStream ffStream( fn.GetFullPath() );
2523 wxStdOutputStream stdStream( ffStream );
2524
2525#if OCC_VERSION_HEX >= 0x070600
2526 BRepTools::Write( shape, stdStream, false, false, TopTools_FormatVersion_VERSION_1 );
2527#else
2528 BRepTools::Write( shape, stdStream );
2529#endif
2530
2531 return true;
2532}
2533
2534
2535bool STEP_PCB_MODEL::WriteXAO( const wxString& aFileName )
2536{
2537 wxFileName fn( aFileName );
2538
2539 wxFFileOutputStream ffStream( fn.GetFullPath() );
2540 wxStdOutputStream file( ffStream );
2541
2542 if( !ffStream.IsOk() )
2543 {
2544 m_reporter->Report( wxString::Format( "Could not open file '%s'", fn.GetFullPath() ),
2546 return false;
2547 }
2548
2549 m_outFmt = OUTPUT_FORMAT::FMT_OUT_XAO;
2550
2551 // s_assy = shape tool for the source
2552 Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
2553
2554 // retrieve assembly as a single shape
2555 const TopoDS_Shape shape = getOneShape( s_assy );
2556
2557 std::map<wxString, std::vector<int>> groups[4];
2558 std::map<wxString, double> groupAreas;
2559 TopExp_Explorer exp;
2560 int faceIndex = 0;
2561
2562 for( exp.Init( shape, TopAbs_FACE ); exp.More(); exp.Next() )
2563 {
2564 TopoDS_Shape subShape = exp.Current();
2565
2566 Bnd_Box bbox;
2567 BRepBndLib::Add( subShape, bbox );
2568
2569 for( const auto& [padKey, pairs] : m_pad_points )
2570 {
2571 for( const auto& pair : pairs )
2572 {
2573 const auto& [point, padTestShape] = pair;
2574
2575 if( bbox.IsOut( point ) )
2576 continue;
2577
2578 BRepAdaptor_Surface surface( TopoDS::Face( subShape ) );
2579
2580 if( surface.GetType() != GeomAbs_Plane )
2581 continue;
2582
2583 BRepExtrema_DistShapeShape dist( padTestShape, subShape );
2584 dist.Perform();
2585
2586 if( !dist.IsDone() )
2587 continue;
2588
2589 if( dist.Value() < Precision::Approximation() )
2590 {
2591 // Push as a face group
2592 groups[2][padKey].push_back( faceIndex );
2593
2594 GProp_GProps system;
2595 BRepGProp::SurfaceProperties( subShape, system );
2596
2597 double surfaceArea = system.Mass() / 1e6; // Convert to meters^2
2598 groupAreas[padKey] += surfaceArea;
2599 }
2600 }
2601 }
2602
2603 faceIndex++;
2604 }
2605
2606 // Based on Gmsh code
2607 file << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << std::endl;
2608 file << "<XAO version=\"1.0\" author=\"KiCad\">" << std::endl;
2609 file << " <geometry name=\"" << fn.GetName() << "\">" << std::endl;
2610 file << " <shape format=\"BREP\"><![CDATA[";
2611#if OCC_VERSION_HEX < 0x070600
2612 BRepTools::Write( shape, file );
2613#else
2614 BRepTools::Write( shape, file, Standard_True, Standard_True, TopTools_FormatVersion_VERSION_1 );
2615#endif
2616 file << "]]></shape>" << std::endl;
2617 file << " <topology>" << std::endl;
2618
2619 TopTools_IndexedMapOfShape mainMap;
2620 TopExp::MapShapes( shape, mainMap );
2621 std::set<int> topo[4];
2622
2623 static const TopAbs_ShapeEnum c_dimShapeTypes[] = { TopAbs_VERTEX, TopAbs_EDGE, TopAbs_FACE,
2624 TopAbs_SOLID };
2625
2626 static const std::string c_dimLabel[] = { "vertex", "edge", "face", "solid" };
2627 static const std::string c_dimLabels[] = { "vertices", "edges", "faces", "solids" };
2628
2629 for( int dim = 0; dim < 4; dim++ )
2630 {
2631 for( exp.Init( shape, c_dimShapeTypes[dim] ); exp.More(); exp.Next() )
2632 {
2633 TopoDS_Shape subShape = exp.Current();
2634 int idx = mainMap.FindIndex( subShape );
2635
2636 if( idx && !topo[dim].count( idx ) )
2637 topo[dim].insert( idx );
2638 }
2639 }
2640
2641 for( int dim = 0; dim <= 3; dim++ )
2642 {
2643 std::string labels = c_dimLabels[dim];
2644 std::string label = c_dimLabel[dim];
2645
2646 file << " <" << labels << " count=\"" << topo[dim].size() << "\">" << std::endl;
2647 int index = 0;
2648
2649 for( auto p : topo[dim] )
2650 {
2651 std::string name( "" );
2652 file << " <" << label << " index=\"" << index << "\" "
2653 << "name=\"" << name << "\" "
2654 << "reference=\"" << p << "\"/>" << std::endl;
2655
2656 index++;
2657 }
2658 file << " </" << labels << ">" << std::endl;
2659 }
2660
2661 file << " </topology>" << std::endl;
2662 file << " </geometry>" << std::endl;
2663 file << " <groups count=\""
2664 << groups[0].size() + groups[1].size() + groups[2].size() + groups[3].size() << "\">"
2665 << std::endl;
2666
2667 int groupNumber = 1;
2668
2669 m_reporter->Report( wxT( "Pad definitions:\n" ), RPT_SEVERITY_DEBUG );
2670 m_reporter->Report( wxT( "Number\tName\tArea (m^2)\n" ), RPT_SEVERITY_DEBUG );
2671
2672 for( int dim = 0; dim <= 3; dim++ )
2673 {
2674 std::string label = c_dimLabel[dim];
2675
2676 for( auto g : groups[dim] )
2677 {
2678 //std::string name = model->getPhysicalName( dim, g.first );
2679 wxString name = g.first;
2680
2681 if( name.empty() )
2682 { // create same unique name as for MED export
2683 std::ostringstream gs;
2684 gs << "G_" << dim << "D_" << g.first;
2685 name = gs.str();
2686 }
2687 file << " <group name=\"" << name << "\" dimension=\"" << label;
2688//#if 1
2689// // Gmsh XAO extension: also save the physical tag, so that XAO can be used
2690// // to serialize OCC geometries, ready to be used by GetDP, GmshFEM & co
2691// file << "\" tag=\"" << g.first;
2692//#endif
2693 file << "\" count=\"" << g.second.size() << "\">" << std::endl;
2694
2695 for( auto index : g.second )
2696 file << " <element index=\"" << index << "\"/>" << std::endl;
2697
2698 file << " </group>" << std::endl;
2699
2700 m_reporter->Report( wxString::Format( "%d\t%s\t%g\n",
2701 groupNumber,
2702 name,
2703 groupAreas[name] ),
2705
2706 groupNumber++;
2707 }
2708 }
2709
2710 m_reporter->Report( wxT( "\n" ), RPT_SEVERITY_DEBUG );
2711
2712 file << " </groups>" << std::endl;
2713 file << " <fields count=\"0\"/>" << std::endl;
2714 file << "</XAO>" << std::endl;
2715
2716 return true;
2717}
2718
2719
2720bool STEP_PCB_MODEL::getModelLabel( const std::string& aFileNameUTF8, VECTOR3D aScale, TDF_Label& aLabel,
2721 bool aSubstituteModels, wxString* aErrorMessage )
2722{
2723 std::string model_key = aFileNameUTF8 + "_" + std::to_string( aScale.x )
2724 + "_" + std::to_string( aScale.y ) + "_" + std::to_string( aScale.z );
2725
2726 MODEL_MAP::const_iterator mm = m_models.find( model_key );
2727
2728 if( mm != m_models.end() )
2729 {
2730 aLabel = mm->second;
2731 return true;
2732 }
2733
2734 aLabel.Nullify();
2735
2736 Handle( TDocStd_Document ) doc;
2737 m_app->NewDocument( "MDTV-XCAF", doc );
2738
2739 wxString fileName( wxString::FromUTF8( aFileNameUTF8.c_str() ) );
2740 MODEL3D_FORMAT_TYPE modelFmt = fileType( aFileNameUTF8.c_str() );
2741
2742 switch( modelFmt )
2743 {
2744 case FMT_IGES:
2745 if( !readIGES( doc, aFileNameUTF8.c_str() ) )
2746 {
2747 m_reporter->Report( wxString::Format( wxT( "readIGES() failed on filename '%s'.\n" ),
2748 fileName ),
2750 return false;
2751 }
2752 break;
2753
2754 case FMT_STEP:
2755 if( !readSTEP( doc, aFileNameUTF8.c_str() ) )
2756 {
2757 m_reporter->Report( wxString::Format( wxT( "readSTEP() failed on filename '%s'.\n" ),
2758 fileName ),
2760 return false;
2761 }
2762 break;
2763
2764 case FMT_STEPZ:
2765 {
2766 // To export a compressed step file (.stpz or .stp.gz file), the best way is to
2767 // decaompress it in a temporaty file and load this temporary file
2768 wxFFileInputStream ifile( fileName );
2769 wxFileName outFile( fileName );
2770
2771 outFile.SetPath( wxStandardPaths::Get().GetTempDir() );
2772 outFile.SetExt( wxT( "step" ) );
2773 wxFileOffset size = ifile.GetLength();
2774
2775 if( size == wxInvalidOffset )
2776 {
2777 m_reporter->Report( wxString::Format( wxT( "getModelLabel() failed on filename '%s'.\n" ),
2778 fileName ),
2780 return false;
2781 }
2782
2783 {
2784 bool success = false;
2785 wxFFileOutputStream ofile( outFile.GetFullPath() );
2786
2787 if( !ofile.IsOk() )
2788 return false;
2789
2790 char* buffer = new char[size];
2791
2792 ifile.Read( buffer, size );
2793 std::string expanded;
2794
2795 try
2796 {
2797 expanded = gzip::decompress( buffer, size );
2798 success = true;
2799 }
2800 catch( ... )
2801 {
2802 m_reporter->Report( wxString::Format( wxT( "failed to decompress '%s'.\n" ),
2803 fileName ),
2805 }
2806
2807 if( expanded.empty() )
2808 {
2809 ifile.Reset();
2810 ifile.SeekI( 0 );
2811 wxZipInputStream izipfile( ifile );
2812 std::unique_ptr<wxZipEntry> zip_file( izipfile.GetNextEntry() );
2813
2814 if( zip_file && !zip_file->IsDir() && izipfile.CanRead() )
2815 {
2816 izipfile.Read( ofile );
2817 success = true;
2818 }
2819 }
2820 else
2821 {
2822 ofile.Write( expanded.data(), expanded.size() );
2823 }
2824
2825 delete[] buffer;
2826 ofile.Close();
2827
2828 if( success )
2829 {
2830 std::string altFileNameUTF8 = TO_UTF8( outFile.GetFullPath() );
2831 success = getModelLabel( altFileNameUTF8, VECTOR3D( 1.0, 1.0, 1.0 ), aLabel, false );
2832 }
2833
2834 return success;
2835 }
2836
2837 break;
2838 }
2839
2840 case FMT_WRL:
2841 case FMT_WRZ:
2842 /* WRL files are preferred for internal rendering, due to superior material properties, etc.
2843 * However they are not suitable for MCAD export.
2844 *
2845 * If a .wrl file is specified, attempt to locate a replacement file for it.
2846 *
2847 * If a valid replacement file is found, the label for THAT file will be associated with
2848 * the .wrl file
2849 */
2850 if( aSubstituteModels )
2851 {
2852 wxFileName wrlName( fileName );
2853
2854 wxString basePath = wrlName.GetPath();
2855 wxString baseName = wrlName.GetName();
2856
2857 // List of alternate files to look for
2858 // Given in order of preference
2859 // (Break if match is found)
2860 wxArrayString alts;
2861
2862 // Step files
2863 alts.Add( wxT( "stp" ) );
2864 alts.Add( wxT( "step" ) );
2865 alts.Add( wxT( "STP" ) );
2866 alts.Add( wxT( "STEP" ) );
2867 alts.Add( wxT( "Stp" ) );
2868 alts.Add( wxT( "Step" ) );
2869 alts.Add( wxT( "stpz" ) );
2870 alts.Add( wxT( "stpZ" ) );
2871 alts.Add( wxT( "STPZ" ) );
2872 alts.Add( wxT( "step.gz" ) );
2873 alts.Add( wxT( "stp.gz" ) );
2874
2875 // IGES files
2876 alts.Add( wxT( "iges" ) );
2877 alts.Add( wxT( "IGES" ) );
2878 alts.Add( wxT( "igs" ) );
2879 alts.Add( wxT( "IGS" ) );
2880
2881 //TODO - Other alternative formats?
2882
2883 for( const auto& alt : alts )
2884 {
2885 wxFileName altFile( basePath, baseName + wxT( "." ) + alt );
2886
2887 if( altFile.IsOk() && altFile.FileExists() )
2888 {
2889 std::string altFileNameUTF8 = TO_UTF8( altFile.GetFullPath() );
2890
2891 // When substituting a STEP/IGS file for VRML, do not apply the VRML scaling
2892 // to the new STEP model. This process of auto-substitution is janky as all
2893 // heck so let's not mix up un-displayed scale factors with potentially
2894 // mis-matched files. And hope that the user doesn't have multiples files
2895 // named "model.wrl" and "model.stp" referring to different parts.
2896 // TODO: Fix model handling in v7. Default models should only be STP.
2897 // Have option to override this in DISPLAY.
2898 if( getModelLabel( altFileNameUTF8, VECTOR3D( 1.0, 1.0, 1.0 ), aLabel, false ) )
2899 {
2900 return true;
2901 }
2902 }
2903 }
2904
2905 // VRML models only work when exporting to glTF
2906 // Also OCCT < 7.9.0 fail to load most VRML 2.0 models because of Switch nodes
2907 if( m_outFmt == OUTPUT_FORMAT::FMT_OUT_GLTF )
2908 {
2909 if( readVRML( doc, aFileNameUTF8.c_str() ) )
2910 {
2911 Handle( XCAFDoc_ShapeTool ) shapeTool =
2912 XCAFDoc_DocumentTool::ShapeTool( doc->Main() );
2913
2914 prefixNames( shapeTool->Label(),
2915 TCollection_ExtendedString( baseName.c_str().AsChar() ) );
2916 }
2917 else
2918 {
2919 m_reporter->Report( wxString::Format( wxT( "readVRML() failed on filename '%s'.\n" ),
2920 fileName ),
2922 return false;
2923 }
2924 }
2925 }
2926 else // Substitution is not allowed
2927 {
2928 if( aErrorMessage )
2929 aErrorMessage->Printf( wxT( "Cannot load any VRML model for this export.\n" ) );
2930
2931 return false;
2932 }
2933
2934 break;
2935
2936 // TODO: implement IDF and EMN converters
2937
2938 default:
2939 m_reporter->Report( wxString::Format( _( "Cannot identify actual file type for '%s'.\n" ),
2940 fileName ),
2942 return false;
2943 }
2944
2945 aLabel = transferModel( doc, m_doc, aScale );
2946
2947 if( aLabel.IsNull() )
2948 {
2949 m_reporter->Report( wxString::Format( _( "Could not transfer model data from file '%s'.\n" ),
2950 fileName ),
2952 return false;
2953 }
2954
2955 // attach the PART NAME ( base filename: note that in principle
2956 // different models may have the same base filename )
2957 wxFileName afile( fileName );
2958 std::string pname( afile.GetName().ToUTF8() );
2959 TCollection_ExtendedString partname( pname.c_str() );
2960 TDataStd_Name::Set( aLabel, partname );
2961
2962 m_models.insert( MODEL_DATUM( model_key, aLabel ) );
2963 ++m_components;
2964 return true;
2965}
2966
2967
2968bool STEP_PCB_MODEL::getModelLocation( bool aBottom, VECTOR2D aPosition, double aRotation,
2969 VECTOR3D aOffset, VECTOR3D aOrientation,
2970 TopLoc_Location& aLocation )
2971{
2972 // Order of operations:
2973 // a. aOrientation is applied -Z*-Y*-X
2974 // b. aOffset is applied
2975 // Top ? add thickness to the Z offset
2976 // c. Bottom ? Rotate on X axis (in contrast to most ECAD which mirror on Y),
2977 // then rotate on +Z
2978 // Top ? rotate on -Z
2979 // d. aPosition is applied
2980 //
2981 // Note: Y axis is inverted in KiCad
2982
2983 gp_Trsf lPos;
2984 lPos.SetTranslation( gp_Vec( aPosition.x, -aPosition.y, 0.0 ) );
2985
2986 // Offset board thickness
2987 aOffset.z += BOARD_OFFSET;
2988
2989 double boardThickness;
2990 double boardZPos;
2991 getBoardBodyZPlacement( boardZPos, boardThickness );
2992 double top = std::max( boardZPos, boardZPos + boardThickness );
2993 double bottom = std::min( boardZPos, boardZPos + boardThickness );
2994
2995 // 3D step models are placed on the top of copper layers.
2996 // This is true for SMD shapes, and perhaps not always true for TH shapes,
2997 // but we use this Z position for any 3D shape.
2998 double f_pos, f_thickness;
2999 getLayerZPlacement( F_Cu, f_pos, f_thickness );
3000 top += f_thickness;
3001 getLayerZPlacement( B_Cu, f_pos, f_thickness );
3002 bottom += f_thickness; // f_thickness is < 0 for B_Cu layer
3003
3004 gp_Trsf lRot;
3005
3006 if( aBottom )
3007 {
3008 aOffset.z -= bottom;
3009 lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), aRotation );
3010 lPos.Multiply( lRot );
3011 lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 1.0, 0.0, 0.0 ) ), M_PI );
3012 lPos.Multiply( lRot );
3013 }
3014 else
3015 {
3016 aOffset.z += top;
3017 lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), aRotation );
3018 lPos.Multiply( lRot );
3019 }
3020
3021 gp_Trsf lOff;
3022 lOff.SetTranslation( gp_Vec( aOffset.x, aOffset.y, aOffset.z ) );
3023 lPos.Multiply( lOff );
3024
3025 gp_Trsf lOrient;
3026 lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), -aOrientation.z );
3027 lPos.Multiply( lOrient );
3028 lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 1.0, 0.0 ) ), -aOrientation.y );
3029 lPos.Multiply( lOrient );
3030 lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 1.0, 0.0, 0.0 ) ), -aOrientation.x );
3031 lPos.Multiply( lOrient );
3032
3033 aLocation = TopLoc_Location( lPos );
3034 return true;
3035}
3036
3037
3038bool STEP_PCB_MODEL::readIGES( Handle( TDocStd_Document )& doc, const char* fname )
3039{
3040 IGESControl_Controller::Init();
3041 IGESCAFControl_Reader reader;
3042 IFSelect_ReturnStatus stat = reader.ReadFile( fname );
3043
3044 if( stat != IFSelect_RetDone )
3045 return false;
3046
3047 // Enable user-defined shape precision
3048 if( !Interface_Static::SetIVal( "read.precision.mode", 1 ) )
3049 return false;
3050
3051 // Set the shape conversion precision to USER_PREC (default 0.0001 has too many triangles)
3052 if( !Interface_Static::SetRVal( "read.precision.val", USER_PREC ) )
3053 return false;
3054
3055 // set other translation options
3056 reader.SetColorMode( true ); // use model colors
3057 reader.SetNameMode( false ); // don't use IGES label names
3058 reader.SetLayerMode( false ); // ignore LAYER data
3059
3060 if( !reader.Transfer( doc ) )
3061 {
3062 if( doc->CanClose() == CDM_CCS_OK )
3063 doc->Close();
3064
3065 return false;
3066 }
3067
3068 // are there any shapes to translate?
3069 if( reader.NbShapes() < 1 )
3070 {
3071 if( doc->CanClose() == CDM_CCS_OK )
3072 doc->Close();
3073
3074 return false;
3075 }
3076
3077 return true;
3078}
3079
3080
3081bool STEP_PCB_MODEL::readSTEP( Handle( TDocStd_Document )& doc, const char* fname )
3082{
3083 STEPCAFControl_Reader reader;
3084 IFSelect_ReturnStatus stat = reader.ReadFile( fname );
3085
3086 if( stat != IFSelect_RetDone )
3087 return false;
3088
3089 // Enable user-defined shape precision
3090 if( !Interface_Static::SetIVal( "read.precision.mode", 1 ) )
3091 return false;
3092
3093 // Set the shape conversion precision to USER_PREC (default 0.0001 has too many triangles)
3094 if( !Interface_Static::SetRVal( "read.precision.val", USER_PREC ) )
3095 return false;
3096
3097 // set other translation options
3098 reader.SetColorMode( true ); // use model colors
3099 reader.SetNameMode( true ); // use label names
3100 reader.SetLayerMode( false ); // ignore LAYER data
3101
3102 if( !reader.Transfer( doc ) )
3103 {
3104 if( doc->CanClose() == CDM_CCS_OK )
3105 doc->Close();
3106
3107 return false;
3108 }
3109
3110 // are there any shapes to translate?
3111 if( reader.NbRootsForTransfer() < 1 )
3112 {
3113 if( doc->CanClose() == CDM_CCS_OK )
3114 doc->Close();
3115
3116 return false;
3117 }
3118
3119 return true;
3120}
3121
3122
3123bool STEP_PCB_MODEL::readVRML( Handle( TDocStd_Document ) & doc, const char* fname )
3124{
3125#if OCC_VERSION_HEX >= 0x070700
3126 VrmlAPI_CafReader reader;
3127 RWMesh_CoordinateSystemConverter conv;
3128 conv.SetInputLengthUnit( 2.54 );
3129 reader.SetCoordinateSystemConverter( conv );
3130 reader.SetDocument( doc );
3131
3132 if( !reader.Perform( TCollection_AsciiString( fname ), Message_ProgressRange() ) )
3133 return false;
3134
3135 return true;
3136#else
3137 return false;
3138#endif
3139}
3140
3141
3142TDF_Label STEP_PCB_MODEL::transferModel( Handle( TDocStd_Document ) & source,
3143 Handle( TDocStd_Document ) & dest, VECTOR3D aScale )
3144{
3145 // transfer data from Source into a top level component of Dest
3146 // s_assy = shape tool for the source
3147 Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( source->Main() );
3148
3149 // retrieve all free shapes within the assembly
3150 TDF_LabelSequence frshapes;
3151 s_assy->GetFreeShapes( frshapes );
3152
3153 // d_assy = shape tool for the destination
3154 Handle( XCAFDoc_ShapeTool ) d_assy = XCAFDoc_DocumentTool::ShapeTool( dest->Main() );
3155
3156 // create a new shape within the destination and set the assembly tool to point to it
3157 TDF_Label d_targetLabel = d_assy->NewShape();
3158
3159 if( frshapes.Size() == 1 )
3160 {
3161 TDocStd_XLinkTool link;
3162 link.Copy( d_targetLabel, frshapes.First() );
3163 }
3164 else
3165 {
3166 // Rare case
3167 for( TDF_Label& s_shapeLabel : frshapes )
3168 {
3169 TDF_Label d_component = d_assy->NewShape();
3170
3171 TDocStd_XLinkTool link;
3172 link.Copy( d_component, s_shapeLabel );
3173
3174 d_assy->AddComponent( d_targetLabel, d_component, TopLoc_Location() );
3175 }
3176 }
3177
3178 if( aScale.x != 1.0 || aScale.y != 1.0 || aScale.z != 1.0 )
3179 rescaleShapes( d_targetLabel, gp_XYZ( aScale.x, aScale.y, aScale.z ) );
3180
3181 return d_targetLabel;
3182}
3183
3184
3185bool STEP_PCB_MODEL::performMeshing( Handle( XCAFDoc_ShapeTool ) & aShapeTool )
3186{
3187 TDF_LabelSequence freeShapes;
3188 aShapeTool->GetFreeShapes( freeShapes );
3189
3190 m_reporter->Report( wxT( "Meshing model\n" ), RPT_SEVERITY_DEBUG );
3191
3192 // GLTF is a mesh format, we have to trigger opencascade to mesh the shapes we composited into the asesmbly
3193 // To mesh models, lets just grab the free shape root and execute on them
3194 for( Standard_Integer i = 1; i <= freeShapes.Length(); ++i )
3195 {
3196 TDF_Label label = freeShapes.Value( i );
3197 TopoDS_Shape shape;
3198 aShapeTool->GetShape( label, shape );
3199
3200 // These deflection values basically affect the accuracy of the mesh generated, a tighter
3201 // deflection will result in larger meshes
3202 // We could make this a tunable parameter, but for now fix it
3203 const Standard_Real linearDeflection = 0.14;
3204 const Standard_Real angularDeflection = DEG2RAD( 30.0 );
3205 BRepMesh_IncrementalMesh mesh( shape, linearDeflection, Standard_False, angularDeflection,
3206 Standard_True );
3207 }
3208
3209 return true;
3210}
3211
3212
3213bool STEP_PCB_MODEL::WriteGLTF( const wxString& aFileName )
3214{
3215 if( !isBoardOutlineValid() )
3216 {
3217 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'.\n" ),
3218 aFileName ),
3220 return false;
3221 }
3222
3223 m_outFmt = OUTPUT_FORMAT::FMT_OUT_GLTF;
3224
3225 performMeshing( m_assy );
3226
3227 wxFileName fn( aFileName );
3228
3229 const char* tmpGltfname = "$tempfile$.glb";
3230 RWGltf_CafWriter cafWriter( tmpGltfname, true );
3231
3232 cafWriter.SetTransformationFormat( RWGltf_WriterTrsfFormat_Compact );
3233 cafWriter.ChangeCoordinateSystemConverter().SetInputLengthUnit( 0.001 );
3234 cafWriter.ChangeCoordinateSystemConverter().SetInputCoordinateSystem(
3235 RWMesh_CoordinateSystem_Zup );
3236#if OCC_VERSION_HEX >= 0x070700
3237 cafWriter.SetParallel( true );
3238#endif
3239 TColStd_IndexedDataMapOfStringString metadata;
3240
3241 metadata.Add( TCollection_AsciiString( "pcb_name" ),
3242 TCollection_ExtendedString( fn.GetName().wc_str() ) );
3243 metadata.Add( TCollection_AsciiString( "source_pcb_file" ),
3244 TCollection_ExtendedString( fn.GetFullName().wc_str() ) );
3245 metadata.Add( TCollection_AsciiString( "generator" ),
3246 TCollection_AsciiString( wxString::Format( wxS( "KiCad %s" ), GetSemanticVersion() ).ToAscii() ) );
3247 metadata.Add( TCollection_AsciiString( "generated_at" ),
3248 TCollection_AsciiString( GetISO8601CurrentDateTime().ToAscii() ) );
3249
3250 bool success = true;
3251
3252 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
3253 wxString currCWD = wxGetCwd();
3254 wxString workCWD = fn.GetPath();
3255
3256 if( !workCWD.IsEmpty() )
3257 wxSetWorkingDirectory( workCWD );
3258
3259 success = cafWriter.Perform( m_doc, metadata, Message_ProgressRange() );
3260
3261 if( success )
3262 {
3263 // Preserve the permissions of the current file
3264 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpGltfname );
3265
3266 if( !wxRenameFile( tmpGltfname, fn.GetFullName(), true ) )
3267 {
3268 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'.\n" ),
3269 tmpGltfname,
3270 fn.GetFullName() ),
3272 success = false;
3273 }
3274 }
3275
3276 wxSetWorkingDirectory( currCWD );
3277
3278 return success;
3279}
3280
3281
3282bool STEP_PCB_MODEL::WritePLY( const wxString& aFileName )
3283{
3284#if OCC_VERSION_HEX < 0x070700
3285 m_reporter->Report( wxT( "PLY export is not supported before OCCT 7.7.0\n" ), RPT_SEVERITY_ERROR );
3286 return false;
3287#else
3288
3289 if( !isBoardOutlineValid() )
3290 {
3291 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'.\n" ),
3292 aFileName ),
3294 return false;
3295 }
3296
3297 m_outFmt = OUTPUT_FORMAT::FMT_OUT_PLY;
3298
3299 performMeshing( m_assy );
3300
3301 wxFileName fn( aFileName );
3302
3303 const char* tmpFname = "$tempfile$.ply";
3304 RWPly_CafWriter cafWriter( tmpFname );
3305
3306 cafWriter.SetFaceId( true ); // TODO: configurable SetPartId/SetFaceId
3307 cafWriter.ChangeCoordinateSystemConverter().SetInputLengthUnit( 0.001 );
3308 cafWriter.ChangeCoordinateSystemConverter().SetInputCoordinateSystem( RWMesh_CoordinateSystem_Zup );
3309
3310 TColStd_IndexedDataMapOfStringString metadata;
3311
3312 metadata.Add( TCollection_AsciiString( "pcb_name" ),
3313 TCollection_ExtendedString( fn.GetName().wc_str() ) );
3314 metadata.Add( TCollection_AsciiString( "source_pcb_file" ),
3315 TCollection_ExtendedString( fn.GetFullName().wc_str() ) );
3316 metadata.Add( TCollection_AsciiString( "generator" ),
3317 TCollection_AsciiString( wxString::Format( wxS( "KiCad %s" ),
3318 GetSemanticVersion() ).ToAscii() ) );
3319 metadata.Add( TCollection_AsciiString( "generated_at" ),
3320 TCollection_AsciiString( GetISO8601CurrentDateTime().ToAscii() ) );
3321
3322 bool success = true;
3323
3324 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
3325 wxString currCWD = wxGetCwd();
3326 wxString workCWD = fn.GetPath();
3327
3328 if( !workCWD.IsEmpty() )
3329 wxSetWorkingDirectory( workCWD );
3330
3331 success = cafWriter.Perform( m_doc, metadata, Message_ProgressRange() );
3332
3333 if( success )
3334 {
3335 // Preserve the permissions of the current file
3336 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpFname );
3337
3338 if( !wxRenameFile( tmpFname, fn.GetFullName(), true ) )
3339 {
3340 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'.\n" ),
3341 tmpFname,
3342 fn.GetFullName() ),
3344 success = false;
3345 }
3346 }
3347
3348 wxSetWorkingDirectory( currCWD );
3349
3350 return success;
3351#endif
3352}
3353
3354
3355bool STEP_PCB_MODEL::WriteSTL( const wxString& aFileName )
3356{
3357 if( !isBoardOutlineValid() )
3358 {
3359 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'.\n" ),
3360 aFileName ),
3362 return false;
3363 }
3364
3365 m_outFmt = OUTPUT_FORMAT::FMT_OUT_STL;
3366
3367 performMeshing( m_assy );
3368
3369 wxFileName fn( aFileName );
3370
3371 const char* tmpFname = "$tempfile$.stl";
3372
3373 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
3374 wxString currCWD = wxGetCwd();
3375 wxString workCWD = fn.GetPath();
3376
3377 if( !workCWD.IsEmpty() )
3378 wxSetWorkingDirectory( workCWD );
3379
3380 bool success = StlAPI_Writer().Write( getOneShape( m_assy ), tmpFname );
3381
3382 if( success )
3383 {
3384 // Preserve the permissions of the current file
3385 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpFname );
3386
3387 if( !wxRenameFile( tmpFname, fn.GetFullName(), true ) )
3388 {
3389 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'.\n" ),
3390 tmpFname,
3391 fn.GetFullName() ),
3393 success = false;
3394 }
3395 }
3396
3397 wxSetWorkingDirectory( currCWD );
3398
3399 return success;
3400}
const char * name
Definition: DXF_plotter.cpp:62
@ ERROR_OUTSIDE
Definition: approximation.h:33
@ ERROR_INSIDE
Definition: approximation.h:34
constexpr EDA_IU_SCALE pcbIUScale
Definition: base_units.h:112
bool IsPrmSpecified(const wxString &aPrmValue)
BOARD_STACKUP_ITEM_TYPE
Definition: board_stackup.h:43
@ BS_ITEM_TYPE_COPPER
Definition: board_stackup.h:45
@ BS_ITEM_TYPE_SILKSCREEN
Definition: board_stackup.h:51
@ BS_ITEM_TYPE_DIELECTRIC
Definition: board_stackup.h:46
@ BS_ITEM_TYPE_SOLDERMASK
Definition: board_stackup.h:49
wxString GetSemanticVersion()
Get the semantic version string for KiCad defined inside the KiCadVersion.cmake file in the variable ...
const wxString & GetShortNetname() const
int GetY() const
Definition: board_item.h:101
int GetX() const
Definition: board_item.h:95
FOOTPRINT * GetParentFootprint() const
Definition: board_item.cpp:97
int GetMaxError() const
Definition: board_item.cpp:138
Manage one layer needed to make a physical board.
Definition: board_stackup.h:96
wxString GetTypeName() const
int GetSublayersCount() const
PCB_LAYER_ID GetBrdLayerId() const
int GetThickness(int aDielectricSubLayer=0) const
BOARD_STACKUP_ITEM_TYPE GetType() const
Manage layers needed to make a physical board.
const std::vector< BOARD_STACKUP_ITEM * > & GetList() const
constexpr coord_type GetLeft() const
Definition: box2.h:228
constexpr coord_type GetRight() const
Definition: box2.h:217
constexpr coord_type GetTop() const
Definition: box2.h:229
constexpr coord_type GetBottom() const
Definition: box2.h:222
wxString GetReferenceAsString() const
Definition: footprint.h:636
A color representation with 4 components: red, green, blue, alpha.
Definition: color4d.h:104
double r
Red component.
Definition: color4d.h:392
double g
Green component.
Definition: color4d.h:393
COLOR4D & Darken(double aFactor)
Makes the color darker by a given factor.
Definition: color4d.h:226
double a
Alpha component.
Definition: color4d.h:395
double b
Blue component.
Definition: color4d.h:394
LSET is a set of PCB_LAYER_IDs.
Definition: lset.h:37
LSEQ Seq(const LSEQ &aSequence) const
Return an LSEQ from the union of this LSET and a desired sequence.
Definition: lset.cpp:296
bool Contains(PCB_LAYER_ID aLayer) const
See if the layer set contains a PCB layer.
Definition: lset.h:63
Definition: pad.h:54
PAD_PROP GetProperty() const
Definition: pad.h:443
LSET GetLayerSet() const override
Return a std::bitset of all layers on which the item physically resides.
Definition: pad.h:437
bool FlashLayer(int aLayer, bool aOnlyCheckIfPermitted=false) const
Check to see whether the pad should be flashed on the specific layer.
Definition: pad.cpp:362
bool IsOnLayer(PCB_LAYER_ID aLayer) const override
Test to see if this object is on the given layer.
Definition: pad.h:786
const VECTOR2I & GetDrillSize() const
Definition: pad.h:305
PAD_ATTRIB GetAttribute() const
Definition: pad.h:440
const wxString & GetNumber() const
Definition: pad.h:136
void TransformShapeToPolygon(SHAPE_POLY_SET &aBuffer, PCB_LAYER_ID aLayer, int aClearance, int aMaxError, ERROR_LOC aErrorLoc=ERROR_INSIDE, bool ignoreLineWidth=false) const override
Convert the pad shape to a closed polygon.
Definition: pad.cpp:2018
std::shared_ptr< SHAPE_SEGMENT > GetEffectiveHoleShape() const override
Return a SHAPE_SEGMENT object representing the pad's hole.
Definition: pad.cpp:598
A pure virtual class used to derive REPORTER objects from.
Definition: reporter.h:73
virtual REPORTER & Report(const wxString &aText, SEVERITY aSeverity=RPT_SEVERITY_UNDEFINED)
Report a string with a given severity.
Definition: reporter.h:102
Definition: seg.h:42
VECTOR2I A
Definition: seg.h:49
VECTOR2I B
Definition: seg.h:50
const VECTOR2I & GetArcMid() const
Definition: shape_arc.h:118
const VECTOR2I & GetP1() const
Definition: shape_arc.h:117
double GetRadius() const
Definition: shape_arc.cpp:894
const VECTOR2I & GetP0() const
Definition: shape_arc.h:116
const VECTOR2I & GetCenter() const
Definition: shape_arc.cpp:849
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
const SHAPE_ARC & Arc(size_t aArc) const
bool IsClosed() const override
void SetClosed(bool aClosed)
Mark the line chain as closed (i.e.
int PointCount() const
Return the number of points (vertices) in this line chain.
ssize_t ArcIndex(size_t aSegment) const
Return the arc index for the given segment index.
void Clear()
Remove all points from the line chain.
const std::optional< INTERSECTION > SelfIntersectingWithArcs() const
Check if the line chain is self-intersecting.
int NextShape(int aPointIndex) const
Return the vertex index of the next shape in the chain, or -1 if aPointIndex is the last shape.
void Append(int aX, int aY, bool aAllowDuplication=false)
Append a new point at the end of the line chain.
const VECTOR2I & CPoint(int aIndex) const
Return a reference to a given point in the line chain.
const SHAPE_LINE_CHAIN Slice(int aStartIndex, int aEndIndex) const
Return a subset of this line chain containing the [start_index, end_index] range of points.
virtual size_t GetSegmentCount() const override
const SEG CSegment(int aIndex) const
Return a constant copy of the aIndex segment in the line chain.
bool IsArcSegment(size_t aSegment) const
void RemoveShape(int aPointIndex)
Remove the shape at the given index from the line chain.
bool IsArcStart(size_t aIndex) const
Represent a set of closed polygons.
void ClearArcs()
Removes all arc references from all the outlines and holes in the polyset.
bool IsEmpty() const
Return true if the set is empty (no polygons at all)
POLYGON & Polygon(int aIndex)
Return the aIndex-th subpolygon in the set.
int FullPointCount() const
Return the number of points in the shape poly set.
int Append(int x, int y, int aOutline=-1, int aHole=-1, bool aAllowDuplication=false)
Appends a vertex at the end of the given outline/hole (default: the last outline)
void Simplify()
Simplify the polyset (merges overlapping polys, eliminates degeneracy/self-intersections)
std::vector< SHAPE_LINE_CHAIN > POLYGON
represents a single polygon outline with holes.
void BooleanIntersection(const SHAPE_POLY_SET &b)
Perform boolean polyset intersection.
int OutlineCount() const
Return the number of outlines in the set.
const POLYGON & CPolygon(int aIndex) const
const std::vector< POLYGON > & CPolygons() const
const SEG & GetSeg() const
int GetWidth() const
OUTPUT_FORMAT m_outFmt
The current output format for created file.
void SetCopperColor(double r, double g, double b)
std::vector< TopoDS_Shape > m_board_silkscreen
wxString m_netFilter
bool CreatePCB(SHAPE_POLY_SET &aOutline, VECTOR2D aOrigin, bool aPushBoardBody)
bool MakeShapeAsThickSegment(TopoDS_Shape &aShape, VECTOR2D aStartPoint, VECTOR2D aEndPoint, double aWidth, double aThickness, double aZposition, const VECTOR2D &aOrigin)
Make a segment shape based on start and end point.
bool WritePLY(const wxString &aFileName)
std::map< wxString, std::vector< TopoDS_Shape > > m_board_copper_vias
std::map< wxString, std::vector< TopoDS_Shape > > m_board_copper_pads
bool WriteSTEP(const wxString &aFileName, bool aOptimize, bool compress)
bool AddComponent(const std::string &aFileName, const std::string &aRefDes, bool aBottom, VECTOR2D aPosition, double aRotation, VECTOR3D aOffset, VECTOR3D aOrientation, VECTOR3D aScale, bool aSubstituteModels=true)
wxString m_pcbName
Name of the PCB, which will most likely be the file name of the path.
std::vector< TopoDS_Shape > m_boardCutouts
std::vector< TopoDS_Shape > m_board_soldermask
bool AddPolygonShapes(const SHAPE_POLY_SET *aPolyShapes, PCB_LAYER_ID aLayer, const VECTOR2D &aOrigin, const wxString &aNetname)
void getBoardBodyZPlacement(double &aZPos, double &aThickness)
TDF_Label m_assy_label
void SetFuseShapes(bool aValue)
bool WriteXAO(const wxString &aFileName)
bool WriteGLTF(const wxString &aFileName)
Write the assembly in binary GLTF Format.
REPORTER * m_reporter
double m_mergeOCCMaxDist
std::vector< TDF_Label > m_pcb_labels
STEP_PCB_MODEL(const wxString &aPcbName, REPORTER *aReporter)
void getLayerZPlacement(const PCB_LAYER_ID aLayer, double &aZPos, double &aThickness)
std::vector< TopoDS_Shape > m_board_outlines
void SetSimplifyShapes(bool aValue)
bool readVRML(Handle(TDocStd_Document) &aDoc, const char *aFname)
void SetPadColor(double r, double g, double b)
void SetEnabledLayers(const LSET &aLayers)
bool performMeshing(Handle(XCAFDoc_ShapeTool) &aShapeTool)
bool MakePolygonAsWall(TopoDS_Shape &aShape, SHAPE_POLY_SET &aPolySet, double aHeight, double aZposition, const VECTOR2D &aOrigin)
Make a polygonal shape to create a vertical wall.
bool readSTEP(Handle(TDocStd_Document) &aDoc, const char *aFname)
Handle(XCAFApp_Application) m_app
bool getModelLocation(bool aBottom, VECTOR2D aPosition, double aRotation, VECTOR3D aOffset, VECTOR3D aOrientation, TopLoc_Location &aLocation)
virtual ~STEP_PCB_MODEL()
double m_copperColor[3]
bool WriteSTL(const wxString &aFileName)
void SetStackup(const BOARD_STACKUP &aStackup)
void SetNetFilter(const wxString &aFilter)
double m_padColor[3]
std::map< wxString, std::vector< TopoDS_Shape > > m_board_copper
bool MakeShapes(std::vector< TopoDS_Shape > &aShapes, const SHAPE_POLY_SET &aPolySet, bool aConvertToArcs, double aThickness, double aZposition, const VECTOR2D &aOrigin)
Convert a SHAPE_POLY_SET to TopoDS_Shape's (polygonal vertical prisms, or flat faces)
bool AddBarrel(const SHAPE_SEGMENT &aShape, PCB_LAYER_ID aLayerTop, PCB_LAYER_ID aLayerBot, bool aVia, const VECTOR2D &aOrigin, const wxString &aNetname)
bool getModelLabel(const std::string &aFileNameUTF8, VECTOR3D aScale, TDF_Label &aLabel, bool aSubstituteModels, wxString *aErrorMessage=nullptr)
Load a 3D model data.
bool readIGES(Handle(TDocStd_Document) &aDoc, const char *aFname)
bool AddHole(const SHAPE_SEGMENT &aShape, int aPlatingThickness, PCB_LAYER_ID aLayerTop, PCB_LAYER_ID aLayerBot, bool aVia, const VECTOR2D &aOrigin, bool aCutCopper, bool aCutBody)
std::vector< TopoDS_Shape > m_copperCutouts
bool CompressSTEP(wxString &inputFile, wxString &outputFile)
MODEL_MAP m_models
std::map< wxString, std::vector< std::pair< gp_Pnt, TopoDS_Shape > > > m_pad_points
void getCopperLayerZPlacement(const PCB_LAYER_ID aLayer, double &aZPos, double &aThickness)
TDF_Label transferModel(Handle(TDocStd_Document)&source, Handle(TDocStd_Document) &dest, VECTOR3D aScale)
bool AddPadShape(const PAD *aPad, const VECTOR2D &aOrigin, bool aVia, SHAPE_POLY_SET *aClipPolygon=nullptr)
std::map< wxString, std::vector< TopoDS_Shape > > m_board_copper_fused
void OCCSetMergeMaxDistance(double aDistance=OCC_MAX_DISTANCE_TO_MERGE_POINTS)
BOARD_STACKUP m_stackup
bool WriteBREP(const wxString &aFileName)
wxString StringFromValue(double aValue, bool aAddUnitLabel=false, EDA_DATA_TYPE aType=EDA_DATA_TYPE::DISTANCE) const
Converts aValue in internal units into a united string.
T EuclideanNorm() const
Compute the Euclidean norm of the vector, which is defined as sqrt(x ** 2 + y ** 2).
Definition: vector2d.h:283
T y
Definition: vector3.h:64
T z
Definition: vector3.h:65
T x
Definition: vector3.h:63
void TransformCircleToPolygon(SHAPE_LINE_CHAIN &aBuffer, const VECTOR2I &aCenter, int aRadius, int aError, ERROR_LOC aErrorLoc, int aMinSegCount=0)
Convert a circle to a polygon, using multiple straight lines.
void TransformOvalToPolygon(SHAPE_POLY_SET &aBuffer, const VECTOR2I &aStart, const VECTOR2I &aEnd, int aWidth, int aError, ERROR_LOC aErrorLoc, int aMinSegCount=0)
Convert a oblong shape to a polygon, using multiple segments.
#define _HKI(x)
#define _(s)
static constexpr EDA_ANGLE ANGLE_360
Definition: eda_angle.h:417
wxString LayerName(int aLayer)
Returns the default display name for a given layer.
Definition: layer_id.cpp:31
bool IsFrontLayer(PCB_LAYER_ID aLayerId)
Layer classification: check if it's a front layer.
Definition: layer_ids.h:767
bool IsBackLayer(PCB_LAYER_ID aLayerId)
Layer classification: check if it's a back layer.
Definition: layer_ids.h:790
bool IsCopperLayer(int aLayerId)
Test whether a layer is a copper layer.
Definition: layer_ids.h:665
PCB_LAYER_ID
A quick note on layer IDs:
Definition: layer_ids.h:60
@ B_Mask
Definition: layer_ids.h:98
@ B_Cu
Definition: layer_ids.h:65
@ F_Mask
Definition: layer_ids.h:97
@ F_SilkS
Definition: layer_ids.h:100
@ B_SilkS
Definition: layer_ids.h:101
@ F_Cu
Definition: layer_ids.h:64
This file contains miscellaneous commonly used macros and functions.
bool DuplicatePermissions(const wxString &aSrc, const wxString &aDest)
Duplicates the file security data from one file to another ensuring that they are the same between bo...
Definition: unix/io.cpp:47
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition: eda_angle.h:400
#define USER_PREC
@ RPT_SEVERITY_WARNING
@ RPT_SEVERITY_ERROR
@ RPT_SEVERITY_DEBUG
static bool addSegment(VRML_LAYER &model, IDF_SEGMENT *seg, int icont, int iseg)
const std::vector< FAB_LAYER_COLOR > & GetStandardColors(BOARD_STACKUP_ITEM_TYPE aType)
wxString NotSpecifiedPrm()
#define KEY_PREPREG
#define KEY_CORE
MODEL3D_FORMAT_TYPE
@ FMT_STEP
@ FMT_IGES
@ FMT_IDF
@ FMT_WRZ
@ FMT_STEPZ
@ FMT_NONE
@ FMT_WRL
@ FMT_EMN
static TopoDS_Shape fuseShapesOrCompound(const TopTools_ListOfShape &aInputShapes, REPORTER *aReporter)
static bool colorFromStackup(BOARD_STACKUP_ITEM_TYPE aType, const wxString &aColorStr, COLOR4D &aColorOut)
static bool makeWireFromChain(BRepLib_MakeWire &aMkWire, const SHAPE_LINE_CHAIN &aChain, double aMergeOCCMaxDist, double aZposition, const VECTOR2D &aOrigin, REPORTER *aReporter)
static Standard_Boolean prefixNames(const TDF_Label &aLabel, const TCollection_ExtendedString &aPrefix)
#define APPROX_DBG(stmt)
static Standard_Boolean rescaleShapes(const TDF_Label &theLabel, const gp_XYZ &aScale)
static constexpr double BOARD_OFFSET
static wxString formatBBox(const BOX2I &aBBox)
static bool fuseShapes(auto &aInputShapes, TopoDS_Shape &aOutShape, REPORTER *aReporter)
static TopoDS_Compound makeCompound(const auto &aInputShapes)
static std::vector< FAB_LAYER_COLOR > s_soldermaskColors
MODEL3D_FORMAT_TYPE fileType(const char *aFileName)
static VECTOR2D CircleCenterFrom3Points(const VECTOR2D &p1, const VECTOR2D &p2, const VECTOR2D &p3)
static SHAPE_LINE_CHAIN approximateLineChainWithArcs(const SHAPE_LINE_CHAIN &aSrc)
static constexpr double USER_ANGLE_PREC
static constexpr double USER_PREC
static TopoDS_Shape getOneShape(Handle(XCAFDoc_ShapeTool) aShapeTool)
static constexpr double OCC_MAX_DISTANCE_TO_MERGE_POINTS
Default distance between points to treat them as separate ones (mm) 0.001 mm or less is a reasonable ...
std::pair< std::string, TDF_Label > MODEL_DATUM
#define CLOSE_STREAM(var)
#define OPEN_ISTREAM(var, name)
wxString UnescapeString(const wxString &aSource)
wxString GetISO8601CurrentDateTime()
#define TO_UTF8(wxstring)
Convert a wxString to a UTF8 encoded C string for all wxWidgets build modes.
Definition: string_utils.h:429
constexpr double IUTomm(int iu) const
Definition: base_units.h:90
constexpr int mmToIU(double mm) const
Definition: base_units.h:92
VECTOR2I center
int radius
VECTOR2I end
SHAPE_CIRCLE circle(c.m_circle_center, c.m_circle_radius)
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
Definition: thread_pool.cpp:30
static thread_pool * tp
Definition: thread_pool.cpp:28
BS::thread_pool thread_pool
Definition: thread_pool.h:31
void RotatePoint(int *pX, int *pY, const EDA_ANGLE &aAngle)
Calculate the new point of coord coord pX, pY, for a rotation center 0, 0.
Definition: trigo.cpp:229
double DEG2RAD(double deg)
Definition: trigo.h:166
VECTOR2< int32_t > VECTOR2I
Definition: vector2d.h:695
VECTOR3< double > VECTOR3D
Definition: vector3.h:230