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 <functional>
29#include <new>
30#include <sstream>
31#include <string>
32#include <utility>
33#include <wx/file.h>
34#include <wx/filename.h>
35#include <wx/filefn.h>
36#include <wx/sstream.h>
37#include <wx/stdpaths.h>
38#include <wx/stream.h>
39#include <wx/string.h>
40#include <wx/zstream.h>
41#include <wx/wfstream.h>
42#include <wx/zipstrm.h>
43#include <wx/stdstream.h>
44#include <wx/crt.h>
45
46#include <decompress.hpp>
47
48#include <thread_pool.h>
49#include <trace_helpers.h>
50#include <board.h>
52#include <footprint.h>
54#include <pad.h>
55#include <pcb_track.h>
56#include <kiplatform/io.h>
57#include <string_utils.h>
58#include <build_version.h>
63#include <reporter.h>
64
66
68#include <pcb_painter.h>
69
70#include "step_pcb_model.h"
71#include "streamwrapper.h"
72
73#include <IGESCAFControl_Reader.hxx>
74#include <IGESCAFControl_Writer.hxx>
75#include <IGESControl_Controller.hxx>
76#include <IGESData_GlobalSection.hxx>
77#include <IGESData_IGESModel.hxx>
78#include <Interface_Static.hxx>
79#include <Quantity_Color.hxx>
80#include <STEPCAFControl_Reader.hxx>
81#include <STEPCAFControl_Writer.hxx>
82#include <APIHeaderSection_MakeHeader.hxx>
83#include <Standard_Failure.hxx>
84#include <Standard_Handle.hxx>
85#include <Standard_Version.hxx>
86#include <TCollection_ExtendedString.hxx>
87#include <TDocStd_Document.hxx>
88#include <TDataStd_Name.hxx>
89#include <TDataStd_TreeNode.hxx>
90#include <TDF_ChildIterator.hxx>
91#include <TDF_LabelSequence.hxx>
92#include <TDF_Tool.hxx>
93#include <TopExp_Explorer.hxx>
94#include <TopoDS.hxx>
95#include <XCAFApp_Application.hxx>
96#include <XCAFDoc.hxx>
97#include <XCAFDoc_DocumentTool.hxx>
98#include <XCAFDoc_ColorTool.hxx>
99#include <XCAFDoc_ShapeTool.hxx>
100#include <XCAFDoc_VisMaterialTool.hxx>
101#include <XCAFDoc_Area.hxx>
102#include <XCAFDoc_Centroid.hxx>
103#include <XCAFDoc_Editor.hxx>
104#include <XCAFDoc_Location.hxx>
105#include <XCAFDoc_Volume.hxx>
106#include "kicad3d_info.h"
107
108#include "KI_XCAFDoc_AssemblyGraph.hxx"
109
110#include <BRep_Tool.hxx>
111#include <BRepMesh_IncrementalMesh.hxx>
112#include <BRepBuilderAPI_GTransform.hxx>
113#include <BRepBuilderAPI_MakeEdge.hxx>
114#include <BRepBuilderAPI_MakeWire.hxx>
115#include <BRepBuilderAPI_MakeFace.hxx>
116#include <BRepExtrema_DistShapeShape.hxx>
117#include <BRepPrimAPI_MakeCone.hxx>
118#include <BRepPrimAPI_MakeCylinder.hxx>
119#include <BRepPrimAPI_MakePrism.hxx>
120#include <BRepTools.hxx>
121#include <BRepLib_MakeWire.hxx>
122#include <BRepAdaptor_Surface.hxx>
123#include <BRepAlgoAPI_Check.hxx>
124#include <BRepAlgoAPI_Cut.hxx>
125#include <BRepAlgoAPI_Fuse.hxx>
126#include <ShapeUpgrade_UnifySameDomain.hxx>
127
128#include <BRepBndLib.hxx>
129#include <Bnd_BoundSortBox.hxx>
130#include <GProp_GProps.hxx>
131#include <BRepGProp.hxx>
132
133#include <Geom_Curve.hxx>
134#include <Geom_TrimmedCurve.hxx>
135
136#include <gp_Ax2.hxx>
137#include <gp_Dir.hxx>
138#include <gp_Pnt.hxx>
139#include <GC_MakeArcOfCircle.hxx>
140#include <GC_MakeCircle.hxx>
141
142#include <RWGltf_CafWriter.hxx>
143#include <StlAPI_Writer.hxx>
144
145#if OCC_VERSION_HEX >= 0x070700
146#include <VrmlAPI_CafReader.hxx>
147#include <RWPly_CafWriter.hxx>
148#endif
149
150#include <macros.h>
152
153static constexpr double USER_PREC = 1e-4;
154static constexpr double USER_ANGLE_PREC = 1e-6;
155
156// nominal offset from the board
157static constexpr double BOARD_OFFSET = 0.05;
158
159// supported file types for 3D models
171
172
173MODEL3D_FORMAT_TYPE fileType( const char* aFileName )
174{
175 wxFileName lfile( wxString::FromUTF8Unchecked( aFileName ) );
176
177 if( !lfile.FileExists() )
178 return FMT_NONE;
179
180 wxString ext = lfile.GetExt().Lower();
181
182 if( ext == wxT( "wrl" ) )
183 return FMT_WRL;
184
185 if( ext == wxT( "wrz" ) )
186 return FMT_WRZ;
187
188 if( ext == wxT( "idf" ) )
189 return FMT_IDF; // component outline
190
191 if( ext == wxT( "emn" ) )
192 return FMT_EMN; // PCB assembly
193
194 if( ext == wxT( "stpz" ) || ext == wxT( "gz" ) )
195 return FMT_STEPZ;
196
197 OPEN_ISTREAM( ifile, aFileName );
198
199 if( ifile.fail() )
200 return FMT_NONE;
201
202 char iline[82];
203 MODEL3D_FORMAT_TYPE format_type = FMT_NONE;
204
205 // The expected header should be the first line.
206 // However some files can have a comment at the beginning of the file
207 // So read up to max_line_count lines to try to find the actual header
208 const int max_line_count = 3;
209
210 for( int ii = 0; ii < max_line_count; ii++ )
211 {
212 memset( iline, 0, 82 );
213 ifile.getline( iline, 82 );
214
215 iline[81] = 0; // ensure NULL termination when string is too long
216
217 // check for STEP in Part 21 format
218 // (this can give false positives since Part 21 is not exclusively STEP)
219 if( !strncmp( iline, "ISO-10303-21;", 13 ) )
220 {
221 format_type = FMT_STEP;
222 break;
223 }
224
225 std::string fstr = iline;
226
227 // check for STEP in XML format
228 // (this can give both false positive and false negatives)
229 if( fstr.find( "urn:oid:1.0.10303." ) != std::string::npos )
230 {
231 format_type = FMT_STEP;
232 break;
233 }
234
235 // Note: this is a very simple test which can yield false positives; the only
236 // sure method for determining if a file *not* an IGES model is to attempt
237 // to load it.
238 if( iline[72] == 'S' && ( iline[80] == 0 || iline[80] == 13 || iline[80] == 10 ) )
239 {
240 format_type = FMT_IGES;
241 break;
242 }
243
244 // Only a comment (starting by "/*") is allowed as header
245 if( strncmp( iline, "/*", 2 ) != 0 ) // not a comment
246 break;
247 }
248
249 CLOSE_STREAM( ifile );
250
251 return format_type;
252}
253
254
256 const VECTOR2D& p3 )
257{
259
260 // Move coordinate origin to p2, to simplify calculations
261 VECTOR2D b = p1 - p2;
262 VECTOR2D d = p3 - p2;
263 double bc = ( b.x * b.x + b.y * b.y ) / 2.0;
264 double cd = ( -d.x * d.x - d.y * d.y ) / 2.0;
265 double det = -b.x * d.y + d.x * b.y;
266
267 // We're fine with divisions by 0
268 det = 1.0 / det;
269 center.x = ( -bc * d.y - cd * b.y ) * det;
270 center.y = ( b.x * cd + d.x * bc ) * det;
271 center += p2;
272
273 return center;
274}
275
276
277#define APPROX_DBG( stmt )
278//#define APPROX_DBG( stmt ) stmt
279
281{
282 // An algo that takes 3 points, calculates a circle center,
283 // then tries to find as many points fitting the circle.
284
285 static const double c_radiusDeviation = 1000.0;
286 static const double c_arcCenterDeviation = 1000.0;
287 static const double c_relLengthDeviation = 0.8;
288 static const int c_last_none = -1000; // Meaning the arc cannot be constructed
289 // Allow larger angles for segments below this size
290 static const double c_smallSize = pcbIUScale.mmToIU( 0.1 );
291 static const double c_circleCloseGap = pcbIUScale.mmToIU( 1.0 );
292
293 APPROX_DBG( std::cout << std::endl );
294
295 if( aSrc.PointCount() < 4 )
296 return aSrc;
297
298 if( !aSrc.IsClosed() )
299 return aSrc; // non-closed polygons are not supported
300
302
303 int jEndIdx = aSrc.PointCount() - 3;
304
305 for( int i = 0; i < aSrc.PointCount(); i++ )
306 {
307 int first = i - 3;
308 int last = c_last_none;
309
310 VECTOR2D p0 = aSrc.CPoint( i - 3 );
311 VECTOR2D p1 = aSrc.CPoint( i - 2 );
312 VECTOR2D p2 = aSrc.CPoint( i - 1 );
313
314 APPROX_DBG( std::cout << i << " " << aSrc.CPoint( i ) << " " << ( i - 3 ) << " "
315 << VECTOR2I( p0 ) << " " << ( i - 2 ) << " " << VECTOR2I( p1 ) << " "
316 << ( i - 1 ) << " " << VECTOR2I( p2 ) << std::endl );
317
318 VECTOR2D v01 = p1 - p0;
319 VECTOR2D v12 = p2 - p1;
320
321 bool defective = false;
322
323 double d01 = v01.EuclideanNorm();
324 double d12 = v12.EuclideanNorm();
325
326 // Check distance differences between 3 first points
327 defective |= std::abs( d01 - d12 ) > ( std::max( d01, d12 ) * c_relLengthDeviation );
328
329 if( !defective )
330 {
331 // Check angles between 3 first points
332 EDA_ANGLE a01( v01 );
333 EDA_ANGLE a12( v12 );
334
335 double a_diff = ( a01 - a12 ).Normalize180().AsDegrees();
336 defective |= std::abs( a_diff ) < 0.1;
337
338 // Larger angles are allowed for smaller geometry
339 double maxAngleDiff = std::max( d01, d12 ) < c_smallSize ? 46.0 : 30.0;
340 defective |= std::abs( a_diff ) >= maxAngleDiff;
341 }
342
343 if( !defective )
344 {
345 // Find last point lying on the circle created from 3 first points
347 double radius = ( p0 - center ).EuclideanNorm();
348 VECTOR2D p_prev = p2;
349 EDA_ANGLE a_prev( v12 );
350
351 for( int j = i; j <= jEndIdx; j++ )
352 {
353 VECTOR2D p_test = aSrc.CPoint( j );
354
355 EDA_ANGLE a_test( p_test - p_prev );
356 double rad_test = ( p_test - center ).EuclideanNorm();
357 double d_tl = ( p_test - p_prev ).EuclideanNorm();
358 double rad_dev = std::abs( radius - rad_test );
359
360 APPROX_DBG( std::cout << " " << j << " " << aSrc.CPoint( j ) << " rad "
361 << int64_t( rad_test ) << " ref " << int64_t( radius )
362 << std::endl );
363
364 if( rad_dev > c_radiusDeviation )
365 {
366 APPROX_DBG( std::cout << " " << j
367 << " Radius deviation too large: " << int64_t( rad_dev )
368 << " > " << c_radiusDeviation << std::endl );
369 break;
370 }
371
372 // Larger angles are allowed for smaller geometry
373 double maxAngleDiff =
374 std::max( std::max( d01, d12 ), d_tl ) < c_smallSize ? 46.0 : 30.0;
375
376 double a_diff_test = ( a_prev - a_test ).Normalize180().AsDegrees();
377 if( std::abs( a_diff_test ) >= maxAngleDiff )
378 {
379 APPROX_DBG( std::cout << " " << j << " Angles differ too much " << a_diff_test
380 << std::endl );
381 break;
382 }
383
384 if( std::abs( d_tl - d01 ) > ( std::max( d_tl, d01 ) * c_relLengthDeviation ) )
385 {
386 APPROX_DBG( std::cout << " " << j << " Lengths differ too much " << d_tl
387 << "; " << d01 << std::endl );
388 break;
389 }
390
391 last = j;
392 p_prev = p_test;
393 a_prev = a_test;
394 }
395 }
396
397 if( last != c_last_none )
398 {
399 // Try to add an arc, testing for self-interference
400 SHAPE_ARC arc( aSrc.CPoint( first ), aSrc.CPoint( ( first + last ) / 2 ),
401 aSrc.CPoint( last ), 0 );
402
403 if( last > aSrc.PointCount() - 3 && !dst.IsArcSegment( 0 ) )
404 {
405 // If we've found an arc at the end, but already added segments at the start, remove them.
406 int toRemove = last - ( aSrc.PointCount() - 3 );
407
408 while( toRemove )
409 {
410 dst.RemoveShape( 0 );
411 toRemove--;
412 }
413 }
414
415 SHAPE_LINE_CHAIN testChain = dst;
416
417 testChain.Append( arc );
418 testChain.Append( aSrc.Slice( last, std::max( last, aSrc.PointCount() - 3 ) ) );
419 testChain.SetClosed( aSrc.IsClosed() );
420
421 if( !testChain.SelfIntersectingWithArcs() )
422 {
423 // Add arc
424 dst.Append( arc );
425
426 APPROX_DBG( std::cout << " Add arc start " << arc.GetP0() << " mid "
427 << arc.GetArcMid() << " end " << arc.GetP1() << std::endl );
428
429 i = last + 3;
430 }
431 else
432 {
433 // Self-interference
434 last = c_last_none;
435
436 APPROX_DBG( std::cout << " Self-intersection check failed" << std::endl );
437 }
438 }
439
440 if( last == c_last_none )
441 {
442 if( first < 0 )
443 jEndIdx = first + aSrc.PointCount();
444
445 // Add point
446 dst.Append( p0 );
447 APPROX_DBG( std::cout << " Add pt " << VECTOR2I( p0 ) << std::endl );
448 }
449 }
450
451 dst.SetClosed( true );
452
453 // Try to merge arcs
454 int iarc0 = dst.ArcIndex( 0 );
455 int iarc1 = dst.ArcIndex( dst.GetSegmentCount() - 1 );
456
457 if( iarc0 != -1 && iarc1 != -1 )
458 {
459 APPROX_DBG( std::cout << "Final arcs " << iarc0 << " " << iarc1 << std::endl );
460
461 if( iarc0 == iarc1 )
462 {
463 SHAPE_ARC arc = dst.Arc( iarc0 );
464
465 VECTOR2D p0 = arc.GetP0();
466 VECTOR2D p1 = arc.GetP1();
467
468 // If we have only one arc and the gap is small, make it a circle
469 if( ( p1 - p0 ).EuclideanNorm() < c_circleCloseGap )
470 {
471 dst.Clear();
472 dst.Append( SHAPE_ARC( arc.GetCenter(), arc.GetP0(), ANGLE_360 ) );
473 }
474 }
475 else
476 {
477 // Merge first and last arcs if they are similar
478 SHAPE_ARC arc0 = dst.Arc( iarc0 );
479 SHAPE_ARC arc1 = dst.Arc( iarc1 );
480
481 VECTOR2D ac0 = arc0.GetCenter();
482 VECTOR2D ac1 = arc1.GetCenter();
483
484 double ar0 = arc0.GetRadius();
485 double ar1 = arc1.GetRadius();
486
487 if( std::abs( ar0 - ar1 ) <= c_radiusDeviation
488 && ( ac0 - ac1 ).EuclideanNorm() <= c_arcCenterDeviation )
489 {
490 dst.RemoveShape( 0 );
491 dst.RemoveShape( -1 );
492
493 SHAPE_ARC merged( arc1.GetP0(), arc1.GetArcMid(), arc0.GetP1(), 0 );
494
495 dst.Append( merged );
496 }
497 }
498 }
499
500 return dst;
501}
502
503
504static TopoDS_Shape getOneShape( Handle( XCAFDoc_ShapeTool ) aShapeTool )
505{
506 TDF_LabelSequence theLabels;
507 aShapeTool->GetFreeShapes( theLabels );
508
509 TopoDS_Shape aShape;
510
511 if( theLabels.Length() == 1 )
512 return aShapeTool->GetShape( theLabels.Value( 1 ) );
513
514 TopoDS_Compound aCompound;
515 BRep_Builder aBuilder;
516 aBuilder.MakeCompound( aCompound );
517
518 for( TDF_LabelSequence::Iterator anIt( theLabels ); anIt.More(); anIt.Next() )
519 {
520 TopoDS_Shape aFreeShape;
521
522 if( !aShapeTool->GetShape( anIt.Value(), aFreeShape ) )
523 continue;
524
525 aBuilder.Add( aCompound, aFreeShape );
526 }
527
528 if( aCompound.NbChildren() > 0 )
529 aShape = aCompound;
530
531 return aShape;
532}
533
534
535// Apply scaling to shapes within theLabel.
536// Based on XCAFDoc_Editor::RescaleGeometry
537static Standard_Boolean rescaleShapes( const TDF_Label& theLabel, const gp_XYZ& aScale )
538{
539 if( theLabel.IsNull() )
540 {
541 Message::SendFail( "Null label." );
542 return Standard_False;
543 }
544
545 if( Abs( aScale.X() ) <= gp::Resolution() || Abs( aScale.Y() ) <= gp::Resolution()
546 || Abs( aScale.Z() ) <= gp::Resolution() )
547 {
548 Message::SendFail( "Scale factor is too small." );
549 return Standard_False;
550 }
551
552 Handle( XCAFDoc_ShapeTool ) aShapeTool = XCAFDoc_DocumentTool::ShapeTool( theLabel );
553
554 if( aShapeTool.IsNull() )
555 {
556 Message::SendFail( "Couldn't find XCAFDoc_ShapeTool attribute." );
557 return Standard_False;
558 }
559
560 Handle( KI_XCAFDoc_AssemblyGraph ) aG = new KI_XCAFDoc_AssemblyGraph( theLabel );
561
562 if( aG.IsNull() )
563 {
564 Message::SendFail( "Couldn't create assembly graph." );
565 return Standard_False;
566 }
567
568 Standard_Boolean anIsDone = Standard_True;
569
570 // clang-format off
571 gp_GTrsf aGTrsf;
572 aGTrsf.SetVectorialPart( gp_Mat( aScale.X(), 0, 0,
573 0, aScale.Y(), 0,
574 0, 0, aScale.Z() ) );
575 // clang-format on
576
577 BRepBuilderAPI_GTransform aBRepTrsf( aGTrsf );
578
579 for( Standard_Integer idx = 1; idx <= aG->NbNodes(); idx++ )
580 {
581 const KI_XCAFDoc_AssemblyGraph::NodeType aNodeType = aG->GetNodeType( idx );
582
583 if( ( aNodeType != KI_XCAFDoc_AssemblyGraph::NodeType_Part )
584 && ( aNodeType != KI_XCAFDoc_AssemblyGraph::NodeType_Occurrence ) )
585 {
586 continue;
587 }
588
589 const TDF_Label& aLabel = aG->GetNode( idx );
590
591 if( aNodeType == KI_XCAFDoc_AssemblyGraph::NodeType_Part )
592 {
593 const TopoDS_Shape aShape = aShapeTool->GetShape( aLabel );
594 aBRepTrsf.Perform( aShape, Standard_True );
595 if( !aBRepTrsf.IsDone() )
596 {
597 Standard_SStream aSS;
598 TCollection_AsciiString anEntry;
599 TDF_Tool::Entry( aLabel, anEntry );
600 aSS << "Shape " << anEntry << " is not scaled!";
601 Message::SendFail( aSS.str().c_str() );
602 anIsDone = Standard_False;
603 return Standard_False;
604 }
605 TopoDS_Shape aScaledShape = aBRepTrsf.Shape();
606 aShapeTool->SetShape( aLabel, aScaledShape );
607
608 // Update sub-shapes
609 TDF_LabelSequence aSubshapes;
610 aShapeTool->GetSubShapes( aLabel, aSubshapes );
611 for( TDF_LabelSequence::Iterator anItSs( aSubshapes ); anItSs.More(); anItSs.Next() )
612 {
613 const TDF_Label& aLSs = anItSs.Value();
614 const TopoDS_Shape aSs = aShapeTool->GetShape( aLSs );
615 const TopoDS_Shape aSs1 = aBRepTrsf.ModifiedShape( aSs );
616 aShapeTool->SetShape( aLSs, aSs1 );
617 }
618
619 // These attributes will be recomputed eventually, but clear them just in case
620 aLabel.ForgetAttribute( XCAFDoc_Area::GetID() );
621 aLabel.ForgetAttribute( XCAFDoc_Centroid::GetID() );
622 aLabel.ForgetAttribute( XCAFDoc_Volume::GetID() );
623 }
624 else if( aNodeType == KI_XCAFDoc_AssemblyGraph::NodeType_Occurrence )
625 {
626 TopLoc_Location aLoc = aShapeTool->GetLocation( aLabel );
627 gp_Trsf aTrsf = aLoc.Transformation();
628 aTrsf.SetTranslationPart( aTrsf.TranslationPart().Multiplied( aScale ) );
629 XCAFDoc_Location::Set( aLabel, aTrsf );
630 }
631 }
632
633 if( !anIsDone )
634 {
635 return Standard_False;
636 }
637
638 aShapeTool->UpdateAssemblies();
639
640 return anIsDone;
641}
642
643
644static bool fuseShapes( auto& aInputShapes, TopoDS_Shape& aOutShape, REPORTER* aReporter )
645{
646 BRepAlgoAPI_Fuse mkFuse;
647 TopTools_ListOfShape shapeArguments, shapeTools;
648
649 for( TopoDS_Shape& sh : aInputShapes )
650 {
651 if( sh.IsNull() )
652 continue;
653
654 if( shapeArguments.IsEmpty() )
655 shapeArguments.Append( sh );
656 else
657 shapeTools.Append( sh );
658 }
659
660 try
661 {
662 mkFuse.SetRunParallel( true );
663 mkFuse.SetToFillHistory( false );
664 mkFuse.SetArguments( shapeArguments );
665 mkFuse.SetTools( shapeTools );
666 mkFuse.Build();
667 }
668 catch( const std::bad_alloc& )
669 {
670 aReporter->Report( _( "Out of memory while fusing shapes. Consider disabling shape fusing, "
671 "reducing the number of objects (e.g., vias), or freeing system memory." ),
673 return false;
674 }
675 catch( const Standard_Failure& e )
676 {
677 aReporter->Report( wxString::Format( _( "OpenCASCADE error while fusing shapes: %s\n"
678 "This may indicate insufficient memory. Consider "
679 "disabling shape fusing or reducing board complexity." ),
680 e.GetMessageString() ),
682 return false;
683 }
684
685 if( mkFuse.HasErrors() || mkFuse.HasWarnings() )
686 {
687 aReporter->Report( _( "Problems encountered while fusing shapes. This operation is "
688 "memory-intensive; insufficient memory may cause failures." ),
690
691 if( mkFuse.HasErrors() )
692 {
693 wxString msg = _( "Errors:\n" );
694 wxStringOutputStream os_stream( &msg );
695 wxStdOutputStream out( os_stream );
696
697 mkFuse.DumpErrors( out );
698 aReporter->Report( msg, RPT_SEVERITY_ERROR );
699 }
700
701 if( mkFuse.HasWarnings() )
702 {
703 wxString msg = _( "Warnings:\n" );
704 wxStringOutputStream os_stream( &msg );
705 wxStdOutputStream out( os_stream );
706
707 mkFuse.DumpWarnings( out );
708 aReporter->Report( msg, RPT_SEVERITY_WARNING );
709 }
710 }
711
712 if( mkFuse.IsDone() )
713 {
714 TopoDS_Shape fusedShape = mkFuse.Shape();
715
716 try
717 {
718 ShapeUpgrade_UnifySameDomain unify( fusedShape, true, true, false );
719 unify.History() = nullptr;
720 unify.Build();
721
722 TopoDS_Shape unifiedShapes = unify.Shape();
723
724 if( unifiedShapes.IsNull() )
725 {
726 aReporter->Report( _( "ShapeUpgrade_UnifySameDomain produced a null shape." ),
728 }
729 else
730 {
731 aOutShape = unifiedShapes;
732 return true;
733 }
734 }
735 catch( const std::bad_alloc& )
736 {
737 aReporter->Report( _( "Out of memory while unifying shape domains. Consider disabling "
738 "shape fusing or reducing the number of objects." ),
740 return false;
741 }
742 catch( const Standard_Failure& e )
743 {
744 aReporter->Report( wxString::Format( _( "OpenCASCADE error while unifying shapes: %s" ),
745 e.GetMessageString() ),
747 return false;
748 }
749 }
750
751 return false;
752}
753
754
755static TopoDS_Compound makeCompound( const auto& aInputShapes )
756{
757 TopoDS_Compound compound;
758 BRep_Builder builder;
759 builder.MakeCompound( compound );
760
761 for( const TopoDS_Shape& shape : aInputShapes )
762 builder.Add( compound, shape );
763
764 return compound;
765}
766
767
768// Try to fuse shapes. If that fails, just add them to a compound
769static TopoDS_Shape fuseShapesOrCompound( const TopTools_ListOfShape& aInputShapes,
770 REPORTER* aReporter )
771{
772 TopoDS_Shape outShape;
773
774 if( aInputShapes.Size() == 1 )
775 return aInputShapes.First();
776
777 if( fuseShapes( aInputShapes, outShape, aReporter ) )
778 return outShape;
779
780 return makeCompound( aInputShapes );
781}
782
783
784// Sets names in assembly to <aPrefix> (<old name>), or to <aPrefix>
785static Standard_Boolean prefixNames( const TDF_Label& aLabel,
786 const TCollection_ExtendedString& aPrefix )
787{
788 Handle( KI_XCAFDoc_AssemblyGraph ) aG = new KI_XCAFDoc_AssemblyGraph( aLabel );
789
790 if( aG.IsNull() )
791 {
792 Message::SendFail( "Couldn't create assembly graph." );
793 return Standard_False;
794 }
795
796 Standard_Boolean anIsDone = Standard_True;
797
798 for( Standard_Integer idx = 1; idx <= aG->NbNodes(); idx++ )
799 {
800 const TDF_Label& lbl = aG->GetNode( idx );
801 Handle( TDataStd_Name ) nameHandle;
802
803 if( lbl.FindAttribute( TDataStd_Name::GetID(), nameHandle ) )
804 {
805 TCollection_ExtendedString name;
806
807 name += aPrefix;
808 name += " (";
809 name += nameHandle->Get();
810 name += ")";
811
812 TDataStd_Name::Set( lbl, name );
813 }
814 else
815 {
816 TDataStd_Name::Set( lbl, aPrefix );
817 }
818 }
819
820 return anIsDone;
821}
822
823
824STEP_PCB_MODEL::STEP_PCB_MODEL( const wxString& aPcbName, REPORTER* aReporter ) :
825 m_reporter( aReporter )
826{
827 m_app = XCAFApp_Application::GetApplication();
828 m_app->NewDocument( "MDTV-XCAF", m_doc );
829 m_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
830 m_assy_label = m_assy->NewShape();
831 m_hasPCB = false;
832 m_simplifyShapes = true;
833 m_components = 0;
837 m_minx = 1.0e10; // absurdly large number; any valid PCB X value will be smaller
838 m_pcbName = aPcbName;
839 m_fuseShapes = false;
840 m_extraPadThickness = true;
842}
843
844
846{
847 if( m_doc->CanClose() == CDM_CCS_OK )
848 m_doc->Close();
849}
850
851
852bool STEP_PCB_MODEL::AddPadShape( const PAD* aPad, const VECTOR2D& aOrigin, bool aVia,
853 SHAPE_POLY_SET* aClipPolygon )
854{
855 const double c_padExtraThickness = 0.005;
856 bool success = true;
857 std::vector<TopoDS_Shape> padShapes;
858 bool castellated = aClipPolygon && aPad->GetProperty() == PAD_PROP::CASTELLATED;
859
860 for( PCB_LAYER_ID pcb_layer : aPad->GetLayerSet().Seq() )
861 {
862 if( !m_enabledLayers.Contains( pcb_layer ) )
863 continue;
864
865 if( pcb_layer == F_Mask || pcb_layer == B_Mask )
866 continue;
867
868 if( !aPad->FlashLayer( pcb_layer ) )
869 continue;
870
871 double Zpos, thickness;
872 getLayerZPlacement( pcb_layer, Zpos, thickness );
873
874 if( !aVia && m_extraPadThickness )
875 {
876 // Pad surface as a separate face for FEM simulations.
877 if( pcb_layer == F_Cu )
878 thickness += c_padExtraThickness;
879 else if( pcb_layer == B_Cu )
880 thickness -= c_padExtraThickness;
881 }
882
883 TopoDS_Shape testShape;
884
885 // Make a shape on copper layers
886 SHAPE_POLY_SET polySet;
887 aPad->TransformShapeToPolygon( polySet, pcb_layer, 0, aPad->GetMaxError(), ERROR_INSIDE );
888
889 if( castellated )
890 {
891 polySet.ClearArcs();
892 polySet.BooleanIntersection( *aClipPolygon );
893 }
894
895 success &= MakeShapes( padShapes, polySet, m_simplifyShapes, thickness, Zpos, aOrigin );
896
897 if( testShape.IsNull() )
898 {
899 std::vector<TopoDS_Shape> testShapes;
900
901 MakeShapes( testShapes, polySet, m_simplifyShapes, 0.0, Zpos + thickness, aOrigin );
902
903 if( testShapes.size() > 0 )
904 testShape = testShapes.front();
905 }
906
907 if( !aVia && m_extraPadThickness && !testShape.IsNull() )
908 {
909 if( pcb_layer == F_Cu || pcb_layer == B_Cu )
910 {
911 wxString name;
912
913 name << "Pad_";
914
915 if( pcb_layer == F_Cu )
916 name << 'F' << '_';
917 else if( pcb_layer == B_Cu )
918 name << 'B' << '_';
919
920 name << aPad->GetParentFootprint()->GetReferenceAsString() << '_'
921 << aPad->GetNumber() << '_' << aPad->GetShortNetname();
922
923 gp_Pnt point( pcbIUScale.IUTomm( aPad->GetX() - aOrigin.x ),
924 -pcbIUScale.IUTomm( aPad->GetY() - aOrigin.y ), Zpos + thickness );
925
926 m_pad_points[name].emplace_back( point, testShape );
927 }
928 }
929 }
930
931 if( aPad->GetAttribute() == PAD_ATTRIB::PTH && aPad->IsOnLayer( F_Cu ) && aPad->IsOnLayer( B_Cu ) )
932 {
933 double f_pos, f_thickness;
934 double b_pos, b_thickness;
935 getLayerZPlacement( F_Cu, f_pos, f_thickness );
936 getLayerZPlacement( B_Cu, b_pos, b_thickness );
937
938 if( !aVia && m_extraPadThickness )
939 {
940 // Pad surface is slightly thicker
941 f_thickness += c_padExtraThickness;
942 b_thickness -= c_padExtraThickness;
943 }
944
945 double top = std::max( f_pos, f_pos + f_thickness );
946 double bottom = std::min( b_pos, b_pos + b_thickness );
947 double hole_height = top - bottom;
948
949 TopoDS_Shape plating;
950
951 std::shared_ptr<SHAPE_SEGMENT> seg_hole = aPad->GetEffectiveHoleShape();
952 double width = std::min( aPad->GetDrillSize().x, aPad->GetDrillSize().y );
953
954 if( !castellated )
955 {
956 if( MakeShapeAsThickSegment( plating, seg_hole->GetSeg().A, seg_hole->GetSeg().B, width,
957 hole_height, bottom, aOrigin ) )
958 {
959 padShapes.push_back( plating );
960 }
961 else
962 {
963 success = false;
964 }
965 }
966 else
967 {
968 // Note:
969 // the truncated hole shape is exported as a vertical filled shape. The hole itself
970 // will be removed later, when all holes are removed from the board
971 SHAPE_POLY_SET polyHole;
972
973 if( seg_hole->GetSeg().A == seg_hole->GetSeg().B ) // Hole is a circle
974 {
975 TransformCircleToPolygon( polyHole, seg_hole->GetSeg().A, width/2,
976 aPad->GetMaxError(), ERROR_OUTSIDE );
977
978 }
979 else
980 {
981 TransformOvalToPolygon( polyHole, seg_hole->GetSeg().A, seg_hole->GetSeg().B, width,
982 aPad->GetMaxError(), ERROR_OUTSIDE );
983 }
984
985 polyHole.ClearArcs();
986 polyHole.BooleanIntersection( *aClipPolygon );
987
988 if( MakePolygonAsWall( plating, polyHole, hole_height, bottom, aOrigin ) )
989 {
990 padShapes.push_back( plating );
991 }
992 else
993 {
994 success = false;
995 }
996 }
997 }
998
999 if( !success ) // Error
1000 m_reporter->Report( _( "OCC error adding pad/via polygon." ), RPT_SEVERITY_ERROR );
1001
1002 if( !padShapes.empty() )
1003 {
1004 // Fuse pad shapes here before fusing them with tracks because OCCT sometimes has trouble
1005 if( m_fuseShapes )
1006 {
1007 TopTools_ListOfShape padShapesList;
1008
1009 for( const TopoDS_Shape& shape : padShapes )
1010 padShapesList.Append( shape );
1011
1012 m_board_copper_pads[aPad->GetNetname()].push_back( fuseShapesOrCompound( padShapesList, m_reporter ) );
1013 }
1014 else
1015 {
1016 for( const TopoDS_Shape& shape : padShapes )
1017 m_board_copper_pads[aPad->GetNetname()].push_back( shape );
1018 }
1019 }
1020
1021 return success;
1022}
1023
1024
1025bool STEP_PCB_MODEL::AddHole( const SHAPE_SEGMENT& aShape, int aPlatingThickness,
1026 PCB_LAYER_ID aLayerTop, PCB_LAYER_ID aLayerBot, bool aVia,
1027 const VECTOR2D& aOrigin, bool aCutCopper, bool aCutBody )
1028{
1029 double margin = 0.001; // a small margin on the Z axix to be sure the hole
1030 // is bigger than the board with copper
1031 // must be > OCC_MAX_DISTANCE_TO_MERGE_POINTS
1032
1033 // Pads are taller by 0.01 mm
1034 if( !aVia && m_extraPadThickness)
1035 margin += 0.01;
1036
1037 double f_pos, f_thickness;
1038 double b_pos, b_thickness;
1039 getLayerZPlacement( aLayerTop, f_pos, f_thickness );
1040 getLayerZPlacement( aLayerBot, b_pos, b_thickness );
1041 double top = std::max( f_pos, f_pos + f_thickness );
1042 double bottom = std::min( b_pos, b_pos + b_thickness );
1043
1044 double holeZsize = ( top - bottom ) + ( margin * 2 );
1045
1046 double boardDrill = aShape.GetWidth();
1047 double copperDrill = boardDrill - aPlatingThickness * 2;
1048
1049 TopoDS_Shape copperHole, boardHole;
1050
1051 if( aCutCopper )
1052 {
1053 if( MakeShapeAsThickSegment( copperHole, aShape.GetSeg().A, aShape.GetSeg().B, copperDrill,
1054 holeZsize, bottom - margin, aOrigin ) )
1055 {
1056 m_copperCutouts.push_back( copperHole );
1057 }
1058 else
1059 {
1060 return false;
1061 }
1062 }
1063
1064 if( aCutBody )
1065 {
1066 if( MakeShapeAsThickSegment( boardHole, aShape.GetSeg().A, aShape.GetSeg().B, boardDrill,
1067 holeZsize, bottom - margin, aOrigin ) )
1068 {
1069 m_boardCutouts.push_back( boardHole );
1070 }
1071 else
1072 {
1073 return false;
1074 }
1075 }
1076
1077 return true;
1078}
1079
1080
1082 PCB_LAYER_ID aLayerBot, bool aVia, const VECTOR2D& aOrigin,
1083 const wxString& aNetname )
1084{
1085 double f_pos, f_thickness;
1086 double b_pos, b_thickness;
1087 getLayerZPlacement( aLayerTop, f_pos, f_thickness );
1088 getLayerZPlacement( aLayerBot, b_pos, b_thickness );
1089 double top = std::max( f_pos, f_pos + f_thickness );
1090 double bottom = std::min( b_pos, b_pos + b_thickness );
1091
1092 TopoDS_Shape plating;
1093
1094 if( !MakeShapeAsThickSegment( plating, aShape.GetSeg().A, aShape.GetSeg().B, aShape.GetWidth(),
1095 ( top - bottom ), bottom, aOrigin ) )
1096 {
1097 return false;
1098 }
1099
1100 if( aVia )
1101 m_board_copper_vias[aNetname].push_back( plating );
1102 else
1103 m_board_copper_pads[aNetname].push_back( plating );
1104
1105 return true;
1106}
1107
1108
1110 PCB_LAYER_ID aLayerEnd, const VECTOR2D& aOrigin )
1111{
1112 // A backdrill removes board material and copper plating between two layers.
1113 // The backdrill typically starts from an outer layer and drills into an inner layer.
1114 // For example, a top backdrill starts at F_Cu and ends at an inner layer.
1115 // A bottom backdrill starts at B_Cu and ends at an inner layer.
1116
1117 double margin = 0.001; // a small margin on the Z axis to ensure the hole
1118 // is bigger than the board section being removed
1119
1120 // Extra margin to extend past outer copper layers to ensure complete annular ring removal
1121 double copperMargin = 0.5; // 0.5mm extra to cut through any copper/pad thickness
1122
1123 double start_pos, start_thickness;
1124 double end_pos, end_thickness;
1125 getLayerZPlacement( aLayerStart, start_pos, start_thickness );
1126 getLayerZPlacement( aLayerEnd, end_pos, end_thickness );
1127
1128 // Calculate the Z extent of the backdrill
1129 double top = std::max( { start_pos, start_pos + start_thickness,
1130 end_pos, end_pos + end_thickness } );
1131 double bottom = std::min( { start_pos, start_pos + start_thickness,
1132 end_pos, end_pos + end_thickness } );
1133
1134 // Extend past outer copper layers if the backdrill reaches them
1135 if( aLayerStart == F_Cu || aLayerEnd == F_Cu )
1136 top += copperMargin;
1137 if( aLayerStart == B_Cu || aLayerEnd == B_Cu )
1138 bottom -= copperMargin;
1139
1140 double holeZsize = ( top - bottom ) + ( margin * 2 );
1141 double holeZpos = bottom - margin;
1142
1143 double backdrillDiameter = aShape.GetWidth();
1144
1145 TopoDS_Shape backdrillHole;
1146
1147 // Create the backdrill hole shape - this cuts the board body
1148 if( MakeShapeAsThickSegment( backdrillHole, aShape.GetSeg().A, aShape.GetSeg().B,
1149 backdrillDiameter, holeZsize, holeZpos, aOrigin ) )
1150 {
1151 m_boardCutouts.push_back( backdrillHole );
1152
1153 // This removes annular rings and barrel copper between the backdrill layers.
1154 m_copperCutouts.push_back( backdrillHole );
1155 }
1156 else
1157 {
1158 return false;
1159 }
1160
1161 return true;
1162}
1163
1164
1165bool STEP_PCB_MODEL::AddCounterbore( const VECTOR2I& aPosition, int aDiameter, int aDepth,
1166 bool aFrontSide, const VECTOR2D& aOrigin )
1167{
1168 wxLogTrace( traceKiCad2Step, wxT( "AddCounterbore: pos=(%d,%d) diameter=%d depth=%d frontSide=%d origin=(%f,%f)" ),
1169 aPosition.x, aPosition.y, aDiameter, aDepth, aFrontSide ? 1 : 0, aOrigin.x, aOrigin.y );
1170
1171 // A counterbore is a cylindrical recess from the top or bottom of the board
1172 if( aDiameter <= 0 || aDepth <= 0 )
1173 {
1174 wxLogTrace( traceKiCad2Step, wxT( "AddCounterbore: REJECTED - invalid diameter=%d or depth=%d" ),
1175 aDiameter, aDepth );
1176 return false;
1177 }
1178
1179 double margin = 0.001; // small margin to ensure clean cuts
1180
1181 // Extra margin to extend past outer copper layers to ensure complete annular ring removal
1182 double copperMargin = 0.5; // 0.5mm extra to cut through any copper/pad thickness
1183
1184 // Get board body position (between copper layers)
1185 double boardZpos, boardThickness;
1186 getBoardBodyZPlacement( boardZpos, boardThickness );
1187
1188 // Get copper layer positions - these extend beyond the board body
1189 double f_pos, f_thickness, b_pos, b_thickness;
1190 getLayerZPlacement( F_Cu, f_pos, f_thickness );
1191 getLayerZPlacement( B_Cu, b_pos, b_thickness );
1192
1193 // Calculate actual outer surfaces including copper
1194 // F_Cu: f_pos is inner surface, f_pos + f_thickness is outer surface (copper extends upward)
1195 // B_Cu: b_pos is inner surface, b_pos + b_thickness is outer surface (thickness is negative, copper extends downward)
1196 double topOuterSurface = std::max( f_pos, f_pos + f_thickness );
1197 double bottomOuterSurface = std::min( b_pos, b_pos + b_thickness );
1198
1199 wxLogTrace( traceKiCad2Step, wxT( "AddCounterbore: boardZpos=%f boardThickness=%f f_pos=%f f_thickness=%f topOuter=%f bottomOuter=%f" ),
1200 boardZpos, boardThickness, f_pos, f_thickness, topOuterSurface, bottomOuterSurface );
1201
1202 // Convert dimensions to mm
1203 double diameter_mm = pcbIUScale.IUTomm( aDiameter );
1204 double depth_mm = pcbIUScale.IUTomm( aDepth );
1205 double radius_mm = diameter_mm / 2.0;
1206
1207 wxLogTrace( traceKiCad2Step, wxT( "AddCounterbore: diameter_mm=%f depth_mm=%f radius_mm=%f" ),
1208 diameter_mm, depth_mm, radius_mm );
1209
1210 // Calculate cylinder position based on which side
1211 // The cylinder must extend past the outer surface to ensure complete copper removal
1212 double cylinderZpos;
1213 double cylinderHeight;
1214
1215 if( aFrontSide )
1216 {
1217 // Counterbore from top - cylinder extends from above outer copper surface down to depth
1218 // Add copperMargin above the surface to ensure complete annular ring removal
1219 cylinderZpos = topOuterSurface - depth_mm - margin;
1220 cylinderHeight = depth_mm + copperMargin + 2 * margin;
1221 }
1222 else
1223 {
1224 // Counterbore from bottom - cylinder extends from below outer copper surface up to depth
1225 // Add copperMargin below the surface to ensure complete annular ring removal
1226 cylinderZpos = bottomOuterSurface - copperMargin - margin;
1227 cylinderHeight = depth_mm + copperMargin + 2 * margin;
1228 }
1229
1230 // Convert position to mm
1231 double posX_mm = pcbIUScale.IUTomm( aPosition.x - aOrigin.x );
1232 double posY_mm = -pcbIUScale.IUTomm( aPosition.y - aOrigin.y );
1233
1234 wxLogTrace( traceKiCad2Step, wxT( "AddCounterbore: posX_mm=%f posY_mm=%f cylinderZpos=%f cylinderHeight=%f" ),
1235 posX_mm, posY_mm, cylinderZpos, cylinderHeight );
1236
1237 try
1238 {
1239 // Create coordinate system for the cylinder
1240 // The cylinder axis is along Z, positioned at the counterbore center
1241 gp_Ax2 axis( gp_Pnt( posX_mm, posY_mm, cylinderZpos ), gp::DZ() );
1242
1243 TopoDS_Shape cylinder = BRepPrimAPI_MakeCylinder( axis, radius_mm, cylinderHeight );
1244
1245 if( cylinder.IsNull() )
1246 {
1247 wxLogTrace( traceKiCad2Step, wxT( "AddCounterbore: FAILED - cylinder shape is null" ) );
1248 m_reporter->Report( _( "Failed to create counterbore cylinder shape" ),
1250 return false;
1251 }
1252
1253 // Add to both board and copper cutouts
1254 m_boardCutouts.push_back( cylinder );
1255 m_copperCutouts.push_back( cylinder );
1256
1257 wxLogTrace( traceKiCad2Step, wxT( "AddCounterbore: SUCCESS - added cylinder. boardCutouts=%zu copperCutouts=%zu" ),
1258 m_boardCutouts.size(), m_copperCutouts.size() );
1259 }
1260 catch( const Standard_Failure& e )
1261 {
1262 wxLogTrace( traceKiCad2Step, wxT( "AddCounterbore: EXCEPTION - %s" ), e.GetMessageString() );
1263 m_reporter->Report( wxString::Format( _( "OCC exception creating counterbore: %s" ),
1264 e.GetMessageString() ),
1266 return false;
1267 }
1268
1269 return true;
1270}
1271
1272
1273bool STEP_PCB_MODEL::AddCountersink( const VECTOR2I& aPosition, int aDiameter, int aDepth,
1274 int aAngle, bool aFrontSide, const VECTOR2D& aOrigin )
1275{
1276 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: pos=(%d,%d) diameter=%d depth=%d angle=%d frontSide=%d origin=(%f,%f)" ),
1277 aPosition.x, aPosition.y, aDiameter, aDepth, aAngle, aFrontSide ? 1 : 0, aOrigin.x, aOrigin.y );
1278
1279 // A countersink is a conical recess from the top or bottom of the board
1280 // The angle parameter is the total cone angle in decidegrees
1281 // (angle between opposite sides of the cone)
1282 if( aDiameter <= 0 || aAngle <= 0 )
1283 {
1284 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: REJECTED - invalid diameter=%d or angle=%d" ),
1285 aDiameter, aAngle );
1286 return false;
1287 }
1288
1289 double margin = 0.001; // small margin to ensure clean cuts
1290
1291 // Extra margin to extend past outer copper layers to ensure complete annular ring removal
1292 double copperMargin = 0.5; // 0.5mm extra to cut through any copper/pad thickness
1293
1294 // Get board body position (between copper layers)
1295 double boardZpos, boardThickness;
1296 getBoardBodyZPlacement( boardZpos, boardThickness );
1297
1298 // Get copper layer positions - these extend beyond the board body
1299 double f_pos, f_thickness, b_pos, b_thickness;
1300 getLayerZPlacement( F_Cu, f_pos, f_thickness );
1301 getLayerZPlacement( B_Cu, b_pos, b_thickness );
1302
1303 // Calculate actual outer surfaces including copper
1304 double topOuterSurface = std::max( f_pos, f_pos + f_thickness );
1305 double bottomOuterSurface = std::min( b_pos, b_pos + b_thickness );
1306
1307 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: boardZpos=%f boardThickness=%f f_pos=%f f_thickness=%f topOuter=%f bottomOuter=%f" ),
1308 boardZpos, boardThickness, f_pos, f_thickness, topOuterSurface, bottomOuterSurface );
1309
1310 // Convert dimensions to mm
1311 double diameter_mm = pcbIUScale.IUTomm( aDiameter );
1312 double radius_mm = diameter_mm / 2.0;
1313
1314 // Convert angle from decidegrees to radians
1315 // aAngle is the total cone angle, so half-angle is used for geometry
1316 double halfAngleRad = ( aAngle / 10.0 ) * M_PI / 180.0 / 2.0;
1317
1318 // If depth is not specified, calculate it from the diameter and angle
1319 // The countersink depth is the full cone height: depth = radius / tan(halfAngle)
1320 double depth_mm;
1321 if( aDepth <= 0 )
1322 {
1323 // Calculate depth from diameter and angle
1324 depth_mm = radius_mm / tan( halfAngleRad );
1325 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: depth not specified, calculated depth_mm=%f from radius=%f and angle" ),
1326 depth_mm, radius_mm );
1327 }
1328 else
1329 {
1330 depth_mm = pcbIUScale.IUTomm( aDepth );
1331 }
1332
1333 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: diameter_mm=%f depth_mm=%f radius_mm=%f halfAngleRad=%f (deg=%f)" ),
1334 diameter_mm, depth_mm, radius_mm, halfAngleRad, halfAngleRad * 180.0 / M_PI );
1335
1336 // Calculate the cone geometry
1337 // For a countersink, R1 (bottom radius) may be 0 (sharp point) or non-zero
1338 // R2 (top radius) is at the surface
1339 // The cone depth determines how deep it goes
1340
1341 // Calculate bottom radius based on depth and angle
1342 // tan(halfAngle) = (R2 - R1) / depth
1343 // If we want the surface radius to be radius_mm and depth to be depth_mm:
1344 // R1 = R2 - depth * tan(halfAngle)
1345 double bottomRadius_mm = radius_mm - depth_mm * tan( halfAngleRad );
1346
1347 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: bottomRadius_mm=%f (before clamp), tan(halfAngle)=%f" ),
1348 bottomRadius_mm, tan( halfAngleRad ) );
1349
1350 if( bottomRadius_mm < 0 )
1351 bottomRadius_mm = 0; // Cone comes to a point before reaching full depth
1352
1353 // Calculate position based on which side
1354 // Extend the cone past the outer surface by copperMargin to ensure complete copper removal
1355 double coneZpos;
1356 double coneHeight = depth_mm + copperMargin + margin;
1357 double r1, r2; // bottom and top radii for BRepPrimAPI_MakeCone
1358
1359 // Convert position to mm
1360 double posX_mm = pcbIUScale.IUTomm( aPosition.x - aOrigin.x );
1361 double posY_mm = -pcbIUScale.IUTomm( aPosition.y - aOrigin.y );
1362
1363 try
1364 {
1365 TopoDS_Shape cone;
1366
1367 if( aFrontSide )
1368 {
1369 // Countersink from top - cone apex points down
1370 // In OCC, cone is built from z=0 to z=H with R1 at z=0 and R2 at z=H
1371 // For a top countersink, we want large radius at top, small at bottom
1372 coneZpos = topOuterSurface - depth_mm - margin;
1373 r1 = bottomRadius_mm; // smaller radius at bottom (deeper into board)
1374 // Extend the top radius to account for the copperMargin extension above the surface
1375 r2 = radius_mm + ( copperMargin + margin ) * tan( halfAngleRad );
1376
1377 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: FRONT - coneZpos=%f r1=%f r2=%f coneHeight=%f" ),
1378 coneZpos, r1, r2, coneHeight );
1379
1380 gp_Ax2 axis( gp_Pnt( posX_mm, posY_mm, coneZpos ), gp::DZ() );
1381 cone = BRepPrimAPI_MakeCone( axis, r1, r2, coneHeight );
1382 }
1383 else
1384 {
1385 // Countersink from bottom - cone apex points up
1386 // For bottom countersink, large radius at bottom, small at top
1387 // Extend below the surface by copperMargin
1388 coneZpos = bottomOuterSurface - copperMargin - margin;
1389 // Extend the bottom radius to account for the copperMargin extension below the surface
1390 r1 = radius_mm + ( copperMargin + margin ) * tan( halfAngleRad );
1391 r2 = bottomRadius_mm; // smaller radius at top (deeper into board)
1392
1393 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: BACK - coneZpos=%f r1=%f r2=%f coneHeight=%f" ),
1394 coneZpos, r1, r2, coneHeight );
1395
1396 gp_Ax2 axis( gp_Pnt( posX_mm, posY_mm, coneZpos ), gp::DZ() );
1397 cone = BRepPrimAPI_MakeCone( axis, r1, r2, coneHeight );
1398 }
1399
1400 if( cone.IsNull() )
1401 {
1402 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: FAILED - cone shape is null" ) );
1403 m_reporter->Report( _( "Failed to create countersink cone shape" ),
1405 return false;
1406 }
1407
1408 // Add to both board and copper cutouts
1409 m_boardCutouts.push_back( cone );
1410 m_copperCutouts.push_back( cone );
1411
1412 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: SUCCESS - added cone. boardCutouts=%zu copperCutouts=%zu" ),
1413 m_boardCutouts.size(), m_copperCutouts.size() );
1414 }
1415 catch( const Standard_Failure& e )
1416 {
1417 wxLogTrace( traceKiCad2Step, wxT( "AddCountersink: EXCEPTION - %s" ), e.GetMessageString() );
1418 m_reporter->Report( wxString::Format( _( "OCC exception creating countersink: %s" ),
1419 e.GetMessageString() ),
1421 return false;
1422 }
1423
1424 return true;
1425}
1426
1427
1428std::map<PCB_LAYER_ID, int> STEP_PCB_MODEL::GetCopperLayerKnockouts( int aDiameter, int aDepth,
1429 int aAngle, bool aFrontSide )
1430{
1431 std::map<PCB_LAYER_ID, int> knockouts;
1432
1433 // Get the outer surface positions (including copper)
1434 double f_pos, f_thickness, b_pos, b_thickness;
1435 getLayerZPlacement( F_Cu, f_pos, f_thickness );
1436 getLayerZPlacement( B_Cu, b_pos, b_thickness );
1437
1438 double topOuterSurface = std::max( f_pos, f_pos + f_thickness );
1439 double bottomOuterSurface = std::min( b_pos, b_pos + b_thickness );
1440
1441 // Convert dimensions to mm
1442 double diameter_mm = pcbIUScale.IUTomm( aDiameter );
1443 double radius_mm = diameter_mm / 2.0;
1444
1445 // Calculate depth in mm
1446 double depth_mm;
1447 double halfAngleRad = 0.0;
1448
1449 if( aAngle > 0 )
1450 {
1451 // Countersink - calculate half angle
1452 halfAngleRad = ( aAngle / 10.0 ) * M_PI / 180.0 / 2.0;
1453
1454 // If depth is not specified for countersink, calculate from diameter and angle
1455 if( aDepth <= 0 )
1456 depth_mm = radius_mm / tan( halfAngleRad );
1457 else
1458 depth_mm = pcbIUScale.IUTomm( aDepth );
1459 }
1460 else
1461 {
1462 // Counterbore - use specified depth
1463 depth_mm = pcbIUScale.IUTomm( aDepth );
1464 }
1465
1466 // Determine the Z range of the feature
1467 double featureTop, featureBottom;
1468
1469 if( aFrontSide )
1470 {
1471 featureTop = topOuterSurface;
1472 featureBottom = topOuterSurface - depth_mm;
1473 }
1474 else
1475 {
1476 featureBottom = bottomOuterSurface;
1477 featureTop = bottomOuterSurface + depth_mm;
1478 }
1479
1480 wxLogTrace( traceKiCad2Step, wxT( "GetCopperLayerKnockouts: featureTop=%f featureBottom=%f depth_mm=%f frontSide=%d" ),
1481 featureTop, featureBottom, depth_mm, aFrontSide ? 1 : 0 );
1482
1483 // Iterate through all copper layers and check if they fall within the feature range
1484 for( const BOARD_STACKUP_ITEM* item : m_stackup.GetList() )
1485 {
1486 if( item->GetType() != BS_ITEM_TYPE_COPPER )
1487 continue;
1488
1489 PCB_LAYER_ID layer = item->GetBrdLayerId();
1490 double layerZ, layerThickness;
1491 getLayerZPlacement( layer, layerZ, layerThickness );
1492
1493 // Get the Z range of this copper layer (both inner and outer surfaces)
1494 double layerTop = std::max( layerZ, layerZ + layerThickness );
1495 double layerBottom = std::min( layerZ, layerZ + layerThickness );
1496
1497 // Check if this layer overlaps with the feature Z range
1498 // A layer is affected if any part of it is within the feature range
1499 bool layerInRange = ( layerTop >= featureBottom && layerBottom <= featureTop );
1500
1501 wxLogTrace( traceKiCad2Step, wxT( "GetCopperLayerKnockouts: layer %d Z=[%f, %f] feature=[%f, %f] inRange=%d" ),
1502 static_cast<int>( layer ), layerBottom, layerTop, featureBottom, featureTop, layerInRange ? 1 : 0 );
1503
1504 if( !layerInRange )
1505 continue;
1506
1507 int knockoutDiameter;
1508
1509 if( aAngle > 0 )
1510 {
1511 // Countersink - calculate diameter at this layer's Z level
1512 // Use the layer surface that's closest to the feature origin surface
1513 double layerSurfaceZ;
1514 if( aFrontSide )
1515 {
1516 // For front-side countersink, use the top surface of the layer
1517 layerSurfaceZ = layerTop;
1518 }
1519 else
1520 {
1521 // For back-side countersink, use the bottom surface of the layer
1522 layerSurfaceZ = layerBottom;
1523 }
1524
1525 // Distance from the surface determines the radius at this Z
1526 double distanceFromSurface;
1527 if( aFrontSide )
1528 distanceFromSurface = topOuterSurface - layerSurfaceZ;
1529 else
1530 distanceFromSurface = layerSurfaceZ - bottomOuterSurface;
1531
1532 // Radius at this depth: r = R - d * tan(halfAngle)
1533 double radiusAtLayer_mm = radius_mm - distanceFromSurface * tan( halfAngleRad );
1534
1535 if( radiusAtLayer_mm <= 0 )
1536 {
1537 wxLogTrace( traceKiCad2Step, wxT( "GetCopperLayerKnockouts: layer %d - countersink tapers to point before this layer" ),
1538 static_cast<int>( layer ) );
1539 continue; // Cone tapers to a point before reaching this layer
1540 }
1541
1542 knockoutDiameter = pcbIUScale.mmToIU( radiusAtLayer_mm * 2.0 );
1543 wxLogTrace( traceKiCad2Step, wxT( "GetCopperLayerKnockouts: layer %d (countersink) - distFromSurface=%f radiusAtLayer=%f diameter=%d" ),
1544 static_cast<int>( layer ), distanceFromSurface, radiusAtLayer_mm, knockoutDiameter );
1545 }
1546 else
1547 {
1548 // Counterbore - constant diameter
1549 knockoutDiameter = aDiameter;
1550 wxLogTrace( traceKiCad2Step, wxT( "GetCopperLayerKnockouts: layer %d (counterbore) - diameter=%d" ),
1551 static_cast<int>( layer ), knockoutDiameter );
1552 }
1553
1554 knockouts[layer] = knockoutDiameter;
1555 }
1556
1557 return knockouts;
1558}
1559
1560
1561void STEP_PCB_MODEL::getLayerZPlacement( const PCB_LAYER_ID aLayer, double& aZPos,
1562 double& aThickness )
1563{
1564 // Offsets above copper in mm
1565 static const double c_silkscreenAboveCopper = 0.04;
1566 static const double c_soldermaskAboveCopper = 0.015;
1567
1568 if( IsCopperLayer( aLayer ) )
1569 {
1570 getCopperLayerZPlacement( aLayer, aZPos, aThickness );
1571 }
1572 else if( IsFrontLayer( aLayer ) )
1573 {
1574 double f_pos, f_thickness;
1575 getCopperLayerZPlacement( F_Cu, f_pos, f_thickness );
1576 double top = std::max( f_pos, f_pos + f_thickness );
1577
1578 if( aLayer == F_SilkS )
1579 aZPos = top + c_silkscreenAboveCopper;
1580 else
1581 aZPos = top + c_soldermaskAboveCopper;
1582
1583 aThickness = 0.0; // Normal points up
1584 }
1585 else if( IsBackLayer( aLayer ) )
1586 {
1587 double b_pos, b_thickness;
1588 getCopperLayerZPlacement( B_Cu, b_pos, b_thickness );
1589 double bottom = std::min( b_pos, b_pos + b_thickness );
1590
1591 if( aLayer == B_SilkS )
1592 aZPos = bottom - c_silkscreenAboveCopper;
1593 else
1594 aZPos = bottom - c_soldermaskAboveCopper;
1595
1596 aThickness = -0.0; // Normal points down
1597 }
1598}
1599
1600
1602 double& aThickness )
1603{
1604 int z = 0;
1605 int thickness = 0;
1606 bool wasPrepreg = false;
1607
1608 const std::vector<BOARD_STACKUP_ITEM*>& materials = m_stackup.GetList();
1609
1610 // Iterate from bottom to top
1611 for( auto it = materials.rbegin(); it != materials.rend(); ++it )
1612 {
1613 const BOARD_STACKUP_ITEM* item = *it;
1614
1615 if( item->GetType() == BS_ITEM_TYPE_COPPER )
1616 {
1617 if( aLayer == B_Cu )
1618 {
1619 // This is the first encountered layer
1620 thickness = -item->GetThickness();
1621 break;
1622 }
1623
1624 // Inner copper position is usually inside prepreg
1625 if( wasPrepreg && item->GetBrdLayerId() != F_Cu )
1626 {
1627 z += item->GetThickness();
1628 thickness = -item->GetThickness();
1629 }
1630 else
1631 {
1632 thickness = item->GetThickness();
1633 }
1634
1635 if( item->GetBrdLayerId() == aLayer )
1636 break;
1637
1638 if( !wasPrepreg && item->GetBrdLayerId() != B_Cu )
1639 z += item->GetThickness();
1640 }
1641 else if( item->GetType() == BS_ITEM_TYPE_DIELECTRIC )
1642 {
1643 wasPrepreg = ( item->GetTypeName() == KEY_PREPREG );
1644
1645 // Dielectric can have sub-layers. Layer 0 is the main layer
1646 // Not frequent, but possible
1647 thickness = 0;
1648 for( int idx = 0; idx < item->GetSublayersCount(); idx++ )
1649 thickness += item->GetThickness( idx );
1650
1651 z += thickness;
1652 }
1653 }
1654
1655 aZPos = pcbIUScale.IUTomm( z );
1656 aThickness = pcbIUScale.IUTomm( thickness );
1657}
1658
1659
1660void STEP_PCB_MODEL::getBoardBodyZPlacement( double& aZPos, double& aThickness )
1661{
1662 double f_pos, f_thickness;
1663 double b_pos, b_thickness;
1664 getLayerZPlacement( F_Cu, f_pos, f_thickness );
1665 getLayerZPlacement( B_Cu, b_pos, b_thickness );
1666 double top = std::min( f_pos, f_pos + f_thickness );
1667 double bottom = std::max( b_pos, b_pos + b_thickness );
1668
1669 aThickness = ( top - bottom );
1670 aZPos = bottom;
1671
1672 wxASSERT( aZPos == 0.0 );
1673}
1674
1675
1676bool STEP_PCB_MODEL::AddExtrudedBody( const SHAPE_POLY_SET& aOutline, bool aBottom, double aStandoff, double aHeight,
1677 const VECTOR2D& aOrigin, uint32_t aColor, EXTRUSION_MATERIAL aMaterial,
1678 const wxString& aRefDes )
1679{
1680 double f_pos, f_thickness;
1681 double b_pos, b_thickness;
1682 getLayerZPlacement( F_Cu, f_pos, f_thickness );
1683 getLayerZPlacement( B_Cu, b_pos, b_thickness );
1684
1685 double boardSurfaceZ;
1686
1687 if( !aBottom )
1688 boardSurfaceZ = std::max( f_pos, f_pos + f_thickness );
1689 else
1690 boardSurfaceZ = std::min( b_pos, b_pos + b_thickness );
1691
1692 double bodyThickness = aHeight - aStandoff;
1693 double zBot;
1694
1695 if( !aBottom )
1696 zBot = boardSurfaceZ + aStandoff;
1697 else
1698 zBot = boardSurfaceZ - aHeight;
1699
1700 m_extruded_bodies.push_back( { {}, {}, aRefDes, aColor, aMaterial } );
1701 return MakeShapes( m_extruded_bodies.back().bodyShapes, aOutline, m_simplifyShapes, bodyThickness, zBot, aOrigin );
1702}
1703
1704
1705bool STEP_PCB_MODEL::AddExtrudedPins( const FOOTPRINT* aFootprint, bool aBottom, double aStandoff,
1706 const VECTOR2D& aOrigin )
1707{
1708 if( aStandoff <= 0.0 )
1709 return false;
1710
1711 SHAPE_POLY_SET pinPoly;
1712
1713 if( !GetExtrusionPinOutline( aFootprint, pinPoly ) )
1714 return false;
1715
1716 const EXTRUDED_3D_BODY* body = aFootprint->GetExtrudedBody();
1717
1718 if( body )
1719 {
1720 VECTOR2I fpPos = aFootprint->GetPosition();
1721 ApplyExtrusionTransform( pinPoly, body, fpPos );
1722 }
1723
1724 double f_pos, f_thickness;
1725 double b_pos, b_thickness;
1726 getLayerZPlacement( F_Cu, f_pos, f_thickness );
1727 getLayerZPlacement( B_Cu, b_pos, b_thickness );
1728
1729 double boardTopZ = std::max( f_pos, f_pos + f_thickness );
1730 double boardBotZ = std::min( b_pos, b_pos + b_thickness );
1731
1732 static const double c_protrusion = 1.0; // 1mm below opposite side
1733
1734 double pinZBot, pinHeight;
1735
1736 if( !aBottom )
1737 {
1738 pinZBot = boardBotZ - c_protrusion;
1739 pinHeight = ( boardTopZ + aStandoff ) - pinZBot;
1740 }
1741 else
1742 {
1743 double pinZTop = boardTopZ + c_protrusion;
1744 pinZBot = boardBotZ - aStandoff;
1745 pinHeight = pinZTop - pinZBot;
1746 }
1747
1748 if( m_extruded_bodies.empty() )
1749 return false;
1750
1751 return MakeShapes( m_extruded_bodies.back().pinShapes, pinPoly, m_simplifyShapes, pinHeight, pinZBot, aOrigin );
1752}
1753
1754
1756 const VECTOR2D& aOrigin, const wxString& aNetname )
1757{
1758 bool success = true;
1759
1760 if( aPolyShapes->IsEmpty() )
1761 return true;
1762
1763 if( !m_enabledLayers.Contains( aLayer ) )
1764 return true;
1765
1766 double z_pos, thickness;
1767 getLayerZPlacement( aLayer, z_pos, thickness );
1768
1769 std::vector<TopoDS_Shape>* targetVec = nullptr;
1770
1771 if( IsCopperLayer( aLayer ) )
1772 targetVec = &m_board_copper[aNetname];
1773 else if( aLayer == F_SilkS )
1774 targetVec = &m_board_front_silk;
1775 else if( aLayer == B_SilkS )
1776 targetVec = &m_board_back_silk;
1777 else if( aLayer == F_Mask )
1778 targetVec = &m_board_front_mask;
1779 else
1780 targetVec = &m_board_back_mask;
1781
1782 if( !MakeShapes( *targetVec, *aPolyShapes, m_simplifyShapes, thickness, z_pos, aOrigin ) )
1783 {
1784 m_reporter->Report( wxString::Format( _( "Could not add shape (%d points) to copper layer %s." ),
1785 aPolyShapes->FullPointCount(),
1786 LayerName( aLayer ) ),
1788
1789 success = false;
1790 }
1791
1792 return success;
1793}
1794
1795
1796bool STEP_PCB_MODEL::AddComponent( const wxString& aBaseName, const wxString& aFileName,
1797 const std::vector<wxString>& aAltFilenames,
1798 const wxString& aRefDes, bool aBottom, VECTOR2D aPosition,
1799 double aRotation, VECTOR3D aOffset, VECTOR3D aOrientation,
1800 VECTOR3D aScale, bool aSubstituteModels )
1801{
1802 if( aFileName.empty() )
1803 {
1804 m_reporter->Report( wxString::Format( _( "No model defined for %s." ), aRefDes ),
1806 return false;
1807 }
1808
1809 m_reporter->Report( wxString::Format( wxT( "Adding component %s." ), aRefDes ), RPT_SEVERITY_DEBUG );
1810
1811 // first retrieve a label
1812 TDF_Label lmodel;
1813 wxString errorMessage;
1814
1815 if( !getModelLabel( aBaseName, aFileName, aAltFilenames, aScale, lmodel, aSubstituteModels,
1816 &errorMessage ) )
1817 {
1818 if( errorMessage.IsEmpty() )
1819 errorMessage.Printf( _( "No model for filename '%s'." ), aFileName );
1820
1821 m_reporter->Report( errorMessage, RPT_SEVERITY_ERROR );
1822 return false;
1823 }
1824
1825 // calculate the Location transform
1826 TopLoc_Location toploc;
1827
1828 if( !getModelLocation( aBottom, aPosition, aRotation, aOffset, aOrientation, toploc ) )
1829 {
1830 m_reporter->Report(
1831 wxString::Format( _( "No location data for filename '%s'." ), aFileName ),
1833 return false;
1834 }
1835
1836 // add the located sub-assembly
1837 TDF_Label llabel = m_assy->AddComponent( m_assy_label, lmodel, toploc );
1838
1839 if( llabel.IsNull() )
1840 {
1841 m_reporter->Report(
1842 wxString::Format( _( "Could not add component with filename '%s'." ), aFileName ),
1844 return false;
1845 }
1846
1847 // attach the RefDes name
1848 TCollection_ExtendedString refdes( aRefDes.utf8_str() );
1849 TDataStd_Name::Set( llabel, refdes );
1850
1851 KICAD3D_INFO::Set( llabel, KICAD3D_MODEL_TYPE::COMPONENT, aRefDes.utf8_string() );
1852
1853 // Rebuild compound shapes for all assemblies so that the newly added component
1854 // contributes to its parent's aggregate TopoDS_Shape. This is required when the
1855 // transferred sub-model was synthesized from a source label tree (via
1856 // XCAFDoc_Editor::Extract), because Extract leaves the assembly shape as an empty
1857 // compound until UpdateAssemblies is invoked. Without this the downstream writers
1858 // (STEPCAFControl_Writer, RWGltf_CafWriter, RWObj_CafWriter) see no geometry for
1859 // the added component.
1860 m_assy->UpdateAssemblies();
1861
1862 return true;
1863}
1864
1865
1867{
1868 m_enabledLayers = aLayers;
1869}
1870
1871
1873{
1874 m_fuseShapes = aValue;
1875}
1876
1877
1879{
1880 m_simplifyShapes = aValue;
1881}
1882
1883
1885{
1886 m_stackup = aStackup;
1887}
1888
1889
1890void STEP_PCB_MODEL::SetNetFilter( const wxString& aFilter )
1891{
1892 m_netFilter = aFilter;
1893}
1894
1895
1897{
1898 m_extraPadThickness = aValue;
1899}
1900
1901
1902void STEP_PCB_MODEL::SetCopperColor( double r, double g, double b )
1903{
1904 m_copperColor[0] = r;
1905 m_copperColor[1] = g;
1906 m_copperColor[2] = b;
1907}
1908
1909
1910void STEP_PCB_MODEL::SetPadColor( double r, double g, double b )
1911{
1912 m_padColor[0] = r;
1913 m_padColor[1] = g;
1914 m_padColor[2] = b;
1915}
1916
1917
1919{
1920 // Ensure a minimal value (in mm)
1921 m_mergeOCCMaxDist = aDistance;
1922}
1923
1924
1926{
1927 return m_pcb_labels.size() > 0;
1928}
1929
1930
1931bool STEP_PCB_MODEL::MakeShapeAsThickSegment( TopoDS_Shape& aShape, const VECTOR2D& aStartPoint,
1932 const VECTOR2D& aEndPoint, double aWidth, double aThickness,
1933 double aZposition, const VECTOR2D& aOrigin )
1934{
1935 // make a wide segment from 2 lines and 2 180 deg arcs
1936 // We need 6 points (3 per arcs)
1937 VECTOR2D coords[6];
1938
1939 // We build a horizontal segment, and after rotate it
1940 double len = ( aEndPoint - aStartPoint ).EuclideanNorm();
1941 double h_width = aWidth/2.0;
1942 // First is end point of first arc, and also start point of first line
1943 coords[0] = VECTOR2D{ 0.0, h_width };
1944
1945 // end point of first line and start point of second arc
1946 coords[1] = VECTOR2D{ len, h_width };
1947
1948 // middle point of second arc
1949 coords[2] = VECTOR2D{ len + h_width, 0.0 };
1950
1951 // start point of second line and end point of second arc
1952 coords[3] = VECTOR2D{ len, -h_width };
1953
1954 // end point of second line and start point of first arc
1955 coords[4] = VECTOR2D{ 0, -h_width };
1956
1957 // middle point of first arc
1958 coords[5] = VECTOR2D{ -h_width, 0.0 };
1959
1960 // Rotate and move to segment position
1961 EDA_ANGLE seg_angle( aEndPoint - aStartPoint );
1962
1963 for( int ii = 0; ii < 6; ii++ )
1964 {
1965 RotatePoint( coords[ii], VECTOR2D{ 0, 0 }, -seg_angle ),
1966 coords[ii] += aStartPoint;
1967 }
1968
1969
1970 // Convert to 3D points
1971 gp_Pnt coords3D[ 6 ];
1972
1973 for( int ii = 0; ii < 6; ii++ )
1974 {
1975 coords3D[ii] = gp_Pnt( pcbIUScale.IUTomm( coords[ii].x - aOrigin.x ),
1976 -pcbIUScale.IUTomm( coords[ii].y - aOrigin.y ), aZposition );
1977 }
1978
1979 // Build OpenCascade shape outlines
1980 BRepBuilderAPI_MakeWire wire;
1981 bool success = true;
1982
1983 // Short segments (distance between end points < m_mergeOCCMaxDist(in mm)) must be
1984 // skipped because OCC merge end points, and a null shape is created
1985 bool short_seg = pcbIUScale.IUTomm( len ) <= m_mergeOCCMaxDist;
1986
1987 try
1988 {
1989 TopoDS_Edge edge;
1990
1991 if( short_seg )
1992 {
1993 Handle( Geom_Circle ) circle = GC_MakeCircle( coords3D[1], // arc1 start point
1994 coords3D[2], // arc1 mid point
1995 coords3D[5] // arc2 mid point
1996 );
1997
1998 edge = BRepBuilderAPI_MakeEdge( circle );
1999 wire.Add( edge );
2000 }
2001 else
2002 {
2003 edge = BRepBuilderAPI_MakeEdge( coords3D[0], coords3D[1] );
2004 wire.Add( edge );
2005
2006 Handle( Geom_TrimmedCurve ) arcOfCircle =
2007 GC_MakeArcOfCircle( coords3D[1], // start point
2008 coords3D[2], // mid point
2009 coords3D[3] // end point
2010 );
2011 edge = BRepBuilderAPI_MakeEdge( arcOfCircle );
2012 wire.Add( edge );
2013
2014 edge = BRepBuilderAPI_MakeEdge( coords3D[3], coords3D[4] );
2015 wire.Add( edge );
2016
2017 Handle( Geom_TrimmedCurve ) arcOfCircle2 =
2018 GC_MakeArcOfCircle( coords3D[4], // start point
2019 coords3D[5], // mid point
2020 coords3D[0] // end point
2021 );
2022 edge = BRepBuilderAPI_MakeEdge( arcOfCircle2 );
2023 wire.Add( edge );
2024 }
2025 }
2026 catch( const Standard_Failure& e )
2027 {
2028 m_reporter->Report( wxString::Format( _( "OCC exception building shape segment: %s" ),
2029 e.GetMessageString() ),
2031 return false;
2032 }
2033
2034 BRepBuilderAPI_MakeFace face;
2035
2036 try
2037 {
2038 gp_Pln plane( coords3D[0], gp::DZ() );
2039 face = BRepBuilderAPI_MakeFace( plane, wire );
2040 }
2041 catch( const Standard_Failure& e )
2042 {
2043 m_reporter->Report( wxString::Format( _( "OCC exception building face: %s" ),
2044 e.GetMessageString() ),
2046 return false;
2047 }
2048
2049 if( aThickness != 0.0 )
2050 {
2051 aShape = BRepPrimAPI_MakePrism( face, gp_Vec( 0, 0, aThickness ) );
2052
2053 if( aShape.IsNull() )
2054 {
2055 m_reporter->Report( _( "Failed to create a prismatic shape" ),
2057 return false;
2058 }
2059 }
2060 else
2061 {
2062 aShape = face;
2063 }
2064
2065 return success;
2066}
2067
2068
2069bool STEP_PCB_MODEL::MakePolygonAsWall( TopoDS_Shape& aShape,
2070 SHAPE_POLY_SET& aPolySet,
2071 double aHeight,
2072 double aZposition, const VECTOR2D& aOrigin )
2073{
2074 std::vector<TopoDS_Shape> testShapes;
2075
2076 bool success = MakeShapes( testShapes, aPolySet, m_simplifyShapes,
2077 aHeight, aZposition, aOrigin );
2078
2079 if( testShapes.size() > 0 )
2080 aShape = testShapes.front();
2081 else
2082 success = false;
2083
2084 return success;
2085}
2086
2087
2088static wxString formatBBox( const BOX2I& aBBox )
2089{
2090 wxString str;
2091 UNITS_PROVIDER unitsProvider( pcbIUScale, EDA_UNITS::MM );
2092
2093 str << "x0: " << unitsProvider.StringFromValue( aBBox.GetLeft(), false ) << "; ";
2094 str << "y0: " << unitsProvider.StringFromValue( aBBox.GetTop(), false ) << "; ";
2095 str << "x1: " << unitsProvider.StringFromValue( aBBox.GetRight(), false ) << "; ";
2096 str << "y1: " << unitsProvider.StringFromValue( aBBox.GetBottom(), false );
2097
2098 return str;
2099}
2100
2101
2102static bool makeWireFromChain( BRepLib_MakeWire& aMkWire, const SHAPE_LINE_CHAIN& aChain,
2103 double aMergeOCCMaxDist, double aZposition, const VECTOR2D& aOrigin,
2104 REPORTER* aReporter )
2105{
2106 auto toPoint =
2107 [&]( const VECTOR2D& aKiCoords ) -> gp_Pnt
2108 {
2109 return gp_Pnt( pcbIUScale.IUTomm( aKiCoords.x - aOrigin.x ),
2110 -pcbIUScale.IUTomm( aKiCoords.y - aOrigin.y ), aZposition );
2111 };
2112
2113 try
2114 {
2115 auto addSegment = [&]( const VECTOR2I& aPt0, const VECTOR2I& aPt1 ) -> bool
2116 {
2117 if( aPt0 == aPt1 )
2118 return false;
2119
2120 gp_Pnt start = toPoint( aPt0 );
2121 gp_Pnt end = toPoint( aPt1 );
2122
2123 BRepBuilderAPI_MakeEdge mkEdge( start, end );
2124
2125 if( !mkEdge.IsDone() || mkEdge.Edge().IsNull() )
2126 {
2127 aReporter->Report( wxString::Format( _( "Failed to make segment edge (%d %d) -> (%d %d), "
2128 "skipping" ),
2129 aPt0.x, aPt0.y,
2130 aPt1.x, aPt1.y ),
2132 }
2133 else
2134 {
2135 aMkWire.Add( mkEdge.Edge() );
2136
2137 if( aMkWire.Error() != BRepLib_WireDone )
2138 {
2139 aReporter->Report( wxString::Format( _( "Failed to add segment edge (%d %d) -> (%d %d)" ),
2140 aPt0.x, aPt0.y,
2141 aPt1.x, aPt1.y ),
2143 return false;
2144 }
2145 }
2146
2147 return true;
2148 };
2149
2150 auto addArc = [&]( const VECTOR2I& aPt0, const SHAPE_ARC& aArc ) -> bool
2151 {
2152 // Do not export too short segments: they create broken shape because OCC thinks
2153 Handle( Geom_Curve ) curve;
2154
2155 if( aArc.GetCentralAngle() == ANGLE_360 )
2156 {
2157 gp_Ax2 axis = gp::XOY();
2158 axis.SetLocation( toPoint( aArc.GetCenter() ) );
2159
2160 curve = GC_MakeCircle( axis, pcbIUScale.IUTomm( aArc.GetRadius() ) ).Value();
2161 }
2162 else
2163 {
2164 curve = GC_MakeArcOfCircle( toPoint( aPt0 ), toPoint( aArc.GetArcMid() ),
2165 toPoint( aArc.GetP1() ) ).Value();
2166 }
2167
2168 if( curve.IsNull() )
2169 return false;
2170
2171 aMkWire.Add( BRepBuilderAPI_MakeEdge( curve ) );
2172
2173 if( !aMkWire.IsDone() )
2174 {
2175 aReporter->Report( wxString::Format( _( "Failed to add arc curve from (%d %d), arc p0 "
2176 "(%d %d), mid (%d %d), p1 (%d %d)" ),
2177 aPt0.x, aPt0.y,
2178 aArc.GetP0().x, aArc.GetP0().y,
2179 aArc.GetArcMid().x, aArc.GetArcMid().y,
2180 aArc.GetP1().x, aArc.GetP1().y ),
2182 return false;
2183 }
2184
2185 return true;
2186 };
2187
2188 VECTOR2I firstPt;
2189 VECTOR2I lastPt;
2190 bool isFirstShape = true;
2191
2192 for( int i = 0; i <= aChain.PointCount() && i != -1; i = aChain.NextShape( i ) )
2193 {
2194 if( i == 0 )
2195 {
2196 if( aChain.IsArcSegment( 0 ) && aChain.IsArcSegment( aChain.PointCount() - 1 )
2197 && aChain.ArcIndex( 0 ) == aChain.ArcIndex( aChain.PointCount() - 1 ) )
2198 {
2199 // Skip first arc (we should encounter it later)
2200 int nextShape = aChain.NextShape( i );
2201
2202 // If nextShape points to the end, then we have a circle.
2203 if( nextShape != -1 )
2204 i = nextShape;
2205 }
2206 }
2207
2208 if( isFirstShape )
2209 lastPt = aChain.CPoint( i );
2210
2211 bool isArc = aChain.IsArcSegment( i );
2212
2213 if( aChain.IsArcStart( i ) )
2214 {
2215 const SHAPE_ARC& currentArc = aChain.Arc( aChain.ArcIndex( i ) );
2216
2217 if( isFirstShape )
2218 {
2219 firstPt = currentArc.GetP0();
2220 lastPt = firstPt;
2221 }
2222
2223 if( addSegment( lastPt, currentArc.GetP0() ) )
2224 lastPt = currentArc.GetP0();
2225
2226 if( addArc( lastPt, currentArc ) )
2227 lastPt = currentArc.GetP1();
2228 }
2229 else if( !isArc )
2230 {
2231 const SEG& seg = aChain.CSegment( i );
2232
2233 if( isFirstShape )
2234 {
2235 firstPt = seg.A;
2236 lastPt = firstPt;
2237 }
2238
2239 if( addSegment( lastPt, seg.A ) )
2240 lastPt = seg.A;
2241
2242 if( addSegment( lastPt, seg.B ) )
2243 lastPt = seg.B;
2244 }
2245
2246 isFirstShape = false;
2247 }
2248
2249 if( lastPt != firstPt && !addSegment( lastPt, firstPt ) )
2250 {
2251 aReporter->Report( wxString::Format( _( "Failed to close wire at %d, %d -> %d, %d **" ),
2252 lastPt.x, lastPt.y,
2253 firstPt.x, firstPt.y ),
2255
2256 return false;
2257 }
2258 }
2259 catch( const Standard_Failure& e )
2260 {
2261 aReporter->Report( wxString::Format( _( "OCC exception creating wire: %s" ),
2262 e.GetMessageString() ),
2264 return false;
2265 }
2266
2267 return true;
2268}
2269
2270
2271bool STEP_PCB_MODEL::MakeShapes( std::vector<TopoDS_Shape>& aShapes, const SHAPE_POLY_SET& aPolySet,
2272 bool aConvertToArcs, double aThickness, double aZposition,
2273 const VECTOR2D& aOrigin )
2274{
2275 SHAPE_POLY_SET workingPoly = aPolySet;
2276 workingPoly.Simplify();
2277
2278 SHAPE_POLY_SET fallbackPoly = workingPoly;
2279
2280 if( aConvertToArcs )
2281 {
2282 SHAPE_POLY_SET approximated = workingPoly;
2283
2284 for( size_t polyId = 0; polyId < approximated.CPolygons().size(); polyId++ )
2285 {
2286 SHAPE_POLY_SET::POLYGON& polygon = approximated.Polygon( polyId );
2287
2288 for( size_t contId = 0; contId < polygon.size(); contId++ )
2289 polygon[contId] = approximateLineChainWithArcs( polygon[contId] );
2290 }
2291
2292 fallbackPoly = workingPoly;
2293 workingPoly = approximated;
2294
2295 // TODO: this is not accurate because it doesn't check arcs.
2296 /*if( approximated.IsSelfIntersecting() )
2297 {
2298 m_reporter->Report( wxString::Format( _( "Approximated polygon self-intersection check failed\n"
2299 "z: %g; bounding box: %s" ) ),
2300 aZposition,
2301 formatBBox( workingPoly.BBox() ) ),
2302 RPT_SEVERITY_ERROR );
2303 }
2304 else
2305 {
2306 fallbackPoly = workingPoly;
2307 workingPoly = approximated;
2308 }*/
2309 }
2310
2311#if 0 // No longer in use
2312 auto toPoint = [&]( const VECTOR2D& aKiCoords ) -> gp_Pnt
2313 {
2314 return gp_Pnt( pcbIUScale.IUTomm( aKiCoords.x - aOrigin.x ),
2315 -pcbIUScale.IUTomm( aKiCoords.y - aOrigin.y ), aZposition );
2316 };
2317#endif
2318
2319 gp_Pln basePlane( gp_Pnt( 0.0, 0.0, aZposition ),
2320 std::signbit( aThickness ) ? -gp::DZ() : gp::DZ() );
2321
2322 for( size_t polyId = 0; polyId < workingPoly.CPolygons().size(); polyId++ )
2323 {
2324 SHAPE_POLY_SET::POLYGON& polygon = workingPoly.Polygon( polyId );
2325
2326 auto tryMakeWire = [this, &aZposition,
2327 &aOrigin]( const SHAPE_LINE_CHAIN& aContour, bool aAllowRetry ) -> TopoDS_Wire
2328 {
2329 TopoDS_Wire wire;
2330 BRepLib_MakeWire mkWire;
2331
2332 makeWireFromChain( mkWire, aContour, m_mergeOCCMaxDist, aZposition, aOrigin, m_reporter );
2333
2334 if( mkWire.IsDone() )
2335 {
2336 wire = mkWire.Wire();
2337 }
2338 else
2339 {
2340 m_reporter->Report(
2341 wxString::Format( _( "Wire not done (contour points %d): OCC error %d\n"
2342 "z: %g; bounding box: %s" ),
2343 static_cast<int>( aContour.PointCount() ),
2344 static_cast<int>( mkWire.Error() ),
2345 formatBBox( aContour.BBox() ) ),
2347 }
2348
2349 if( !wire.IsNull() )
2350 {
2351 BRepAlgoAPI_Check check( wire, false, true );
2352
2353 if( !check.IsValid() )
2354 {
2355 m_reporter->Report( wxString::Format( _( "Wire self-interference check failed\n"
2356 "z: %g; bounding box: %s" ),
2357 aZposition,
2358 formatBBox( aContour.BBox() ) ),
2360
2361 wire.Nullify();
2362 }
2363 }
2364
2365 return wire;
2366 };
2367
2368 BRepBuilderAPI_MakeFace mkFace;
2369
2370 for( size_t contId = 0; contId < polygon.size(); contId++ )
2371 {
2372 try
2373 {
2374 // We allow retry when trying to convert polygon[contId] when a convert error
2375 // happens, using an equivalent polygon shape.
2376 bool allow_retry = aConvertToArcs ? true : false;
2377
2378 TopoDS_Wire wire = tryMakeWire( polygon[contId], allow_retry );
2379
2380 if( aConvertToArcs && wire.IsNull() )
2381 {
2382 m_reporter->Report( wxString::Format( _( "Using non-simplified polygon." ) ),
2384
2385 // Fall back to original shape. Do not allow retry
2386 allow_retry = false;
2387 wire = tryMakeWire( fallbackPoly.CPolygon( polyId )[contId], allow_retry );
2388 }
2389
2390 if( contId == 0 ) // Outline
2391 {
2392 if( !wire.IsNull() )
2393 {
2394 if( basePlane.Axis().Direction().Z() < 0 )
2395 wire.Reverse();
2396
2397 mkFace = BRepBuilderAPI_MakeFace( basePlane, wire );
2398 }
2399 else
2400 {
2401 m_reporter->Report( wxString::Format( wxT( "** Outline skipped **\n"
2402 "z: %g; bounding box: %s" ),
2403 aZposition,
2404 formatBBox( polygon[contId].BBox() ) ),
2406 break;
2407 }
2408 }
2409 else // Hole
2410 {
2411 if( !wire.IsNull() )
2412 {
2413 if( basePlane.Axis().Direction().Z() > 0 )
2414 wire.Reverse();
2415
2416 mkFace.Add( wire );
2417 }
2418 else
2419 {
2420 m_reporter->Report( wxString::Format( wxT( "** Hole skipped **\n"
2421 "z: %g; bounding box: %s" ),
2422 aZposition,
2423 formatBBox( polygon[contId].BBox() ) ),
2425 }
2426 }
2427 }
2428 catch( const Standard_Failure& e )
2429 {
2430 m_reporter->Report( wxString::Format( _( "OCC exception creating contour %d: %s" ),
2431 static_cast<int>( contId ),
2432 e.GetMessageString() ),
2434 return false;
2435 }
2436 }
2437
2438 if( mkFace.IsDone() )
2439 {
2440 TopoDS_Shape faceShape = mkFace.Shape();
2441
2442 if( aThickness != 0.0 )
2443 {
2444 TopoDS_Shape prism = BRepPrimAPI_MakePrism( faceShape, gp_Vec( 0, 0, aThickness ) );
2445 aShapes.push_back( prism );
2446
2447 if( prism.IsNull() )
2448 {
2449 m_reporter->Report( _( "Failed to create a prismatic shape" ), RPT_SEVERITY_ERROR );
2450 return false;
2451 }
2452 }
2453 else
2454 {
2455 aShapes.push_back( faceShape );
2456 }
2457 }
2458 else
2459 {
2460 m_reporter->Report( _( "** Face skipped **" ), RPT_SEVERITY_DEBUG );
2461 }
2462 }
2463
2464 return true;
2465}
2466
2467
2468// These colors are based on 3D viewer's colors and are different to "gbrjobColors"
2469static std::vector<FAB_LAYER_COLOR> s_soldermaskColors = {
2470 { NotSpecifiedPrm(), wxColor( 20, 51, 36 ) }, // Not specified, not in .gbrjob file
2471 { _HKI( "Green" ), wxColor( 20, 51, 36 ) }, // used in .gbrjob file
2472 { _HKI( "Red" ), wxColor( 181, 19, 21 ) }, // used in .gbrjob file
2473 { _HKI( "Blue" ), wxColor( 2, 59, 162 ) }, // used in .gbrjob file
2474 { _HKI( "Purple" ), wxColor( 32, 2, 53 ) }, // used in .gbrjob file
2475 { _HKI( "Black" ), wxColor( 11, 11, 11 ) }, // used in .gbrjob file
2476 { _HKI( "White" ), wxColor( 245, 245, 245 ) }, // used in .gbrjob file
2477 { _HKI( "Yellow" ), wxColor( 194, 195, 0 ) }, // used in .gbrjob file
2478 { _HKI( "User defined" ), wxColor( 128, 128, 128 ) } // Free; the name is a dummy name here
2479};
2480
2481
2482static bool colorFromStackup( BOARD_STACKUP_ITEM_TYPE aType, const wxString& aColorStr,
2483 COLOR4D& aColorOut )
2484{
2485 if( !IsPrmSpecified( aColorStr ) )
2486 return false;
2487
2488 if( aColorStr.StartsWith( wxT( "#" ) ) ) // User defined color
2489 {
2490 aColorOut = COLOR4D( aColorStr );
2491 return true;
2492 }
2493 else
2494 {
2495 const std::vector<FAB_LAYER_COLOR>& colors =
2496 ( aType == BS_ITEM_TYPE_SOLDERMASK || aType == BS_ITEM_TYPE_SILKSCREEN )
2498 : GetStandardColors( aType );
2499
2500 for( const FAB_LAYER_COLOR& fabColor : colors )
2501 {
2502 if( fabColor.GetName() == aColorStr )
2503 {
2504 aColorOut = fabColor.GetColor( aType );
2505 return true;
2506 }
2507 }
2508 }
2509
2510 return false;
2511}
2512
2513
2514bool STEP_PCB_MODEL::CreatePCB( SHAPE_POLY_SET& aOutline, const VECTOR2D& aOrigin, bool aPushBoardBody )
2515{
2516 if( m_hasPCB )
2517 {
2518 if( !isBoardOutlineValid() )
2519 return false;
2520
2521 return true;
2522 }
2523
2525
2526 Handle( XCAFDoc_VisMaterialTool ) visMatTool = XCAFDoc_DocumentTool::VisMaterialTool( m_doc->Main() );
2527
2528 m_hasPCB = true; // whether or not operations fail we note that CreatePCB has been invoked
2529
2530 // Support for more than one main outline (more than one board)
2531 m_reporter->Report( wxString::Format( wxT( "Build board outlines (%d outlines) with %d points." ),
2532 aOutline.OutlineCount(),
2533 aOutline.FullPointCount() ),
2535
2536 double boardThickness;
2537 double boardZPos;
2538 getBoardBodyZPlacement( boardZPos, boardThickness );
2539
2540#if 1
2541 // This code should work, and it is working most of time
2542 // However there are issues if the main outline is a circle with holes:
2543 // holes from vias and pads are not working
2544 // see bug https://gitlab.com/kicad/code/kicad/-/issues/17446
2545 // (Holes are missing from STEP export with circular PCB outline)
2546 // Hard to say if the bug is in our code or in OCC 7.7
2547 if( !MakeShapes( m_board_outlines, aOutline, false, boardThickness, boardZPos, aOrigin ) )
2548 {
2549 // Error
2550 m_reporter->Report( _( "OCC error creating main outline." ), RPT_SEVERITY_ERROR );
2551 }
2552#else
2553 // Workaround for bug #17446 Holes are missing from STEP export with circular PCB outline
2554 for( const SHAPE_POLY_SET::POLYGON& polygon : aOutline.CPolygons() )
2555 {
2556 for( size_t contId = 0; contId < polygon.size(); contId++ )
2557 {
2558 const SHAPE_LINE_CHAIN& contour = polygon[contId];
2559 SHAPE_POLY_SET polyset;
2560 polyset.Append( contour );
2561
2562 if( contId == 0 ) // main Outline
2563 {
2564 if( !MakeShapes( m_board_outlines, polyset, false, boardThickness, boardZPos,
2565 aOrigin ) )
2566 {
2567 m_reporter->Report( _( "OCC error creating main outline." ),
2569 }
2570 }
2571 else // Hole inside the main outline
2572 {
2573 if( !MakeShapes( m_boardCutouts, polyset, false, boardThickness, boardZPos,
2574 aOrigin ) )
2575 {
2576 m_reporter->Report( _( "OCC error creating hole in main outline." ),
2578 }
2579 }
2580 }
2581 }
2582#endif
2583
2584 // Even if we've disabled board body export, we still need the shapes for bounding box calculations.
2585 Bnd_Box brdBndBox;
2586
2587 for( const TopoDS_Shape& brdShape : m_board_outlines )
2588 BRepBndLib::Add( brdShape, brdBndBox );
2589
2590 // subtract cutouts (if any)
2591 m_reporter->Report( wxString::Format( wxT( "Build board cutouts and holes (%d hole(s))." ),
2592 (int) ( m_boardCutouts.size() + m_copperCutouts.size() ) ),
2594
2595 auto buildBSB =
2596 [&brdBndBox]( std::vector<TopoDS_Shape>& input, Bnd_BoundSortBox& bsbHoles,
2597 std::vector<Bnd_Box>& holeBoxes )
2598 {
2599 // We need to encompass every location we'll need to test in the global bbox,
2600 // otherwise Bnd_BoundSortBox doesn't work near the boundaries.
2601 Bnd_Box brdWithHolesBndBox = brdBndBox;
2602
2603 Handle( Bnd_HArray1OfBox ) holeBoxSet = new Bnd_HArray1OfBox( 0, input.size() - 1 );
2604 holeBoxes.resize( input.size() );
2605
2606 for( size_t i = 0; i < input.size(); i++ )
2607 {
2608 Bnd_Box bbox;
2609 BRepBndLib::Add( input[i], bbox );
2610 brdWithHolesBndBox.Add( bbox );
2611 ( *holeBoxSet )[i] = bbox;
2612 holeBoxes[i] = bbox;
2613 }
2614
2615 bsbHoles.Initialize( brdWithHolesBndBox, holeBoxSet );
2616 };
2617
2618 auto subtractShapesMap =
2619 [&tp, this]( const wxString& aWhat, std::map<wxString, std::vector<TopoDS_Shape>>& aShapesMap,
2620 std::vector<TopoDS_Shape>& aHolesList, Bnd_BoundSortBox& aBSBHoles,
2621 const std::vector<Bnd_Box>& aHoleBoxes )
2622 {
2623 m_reporter->Report( wxString::Format( _( "Subtracting holes for %s" ), aWhat ),
2625
2626 for( auto& [netname, vec] : aShapesMap )
2627 {
2628 std::mutex mutex;
2629
2630 auto subtractLoopFn = [&]( const int shapeId )
2631 {
2632 TopoDS_Shape& shape = vec[shapeId];
2633
2634 Bnd_Box shapeBbox;
2635 BRepBndLib::Add( shape, shapeBbox );
2636
2637 TopTools_ListOfShape holelist;
2638
2639 {
2640 std::unique_lock lock( mutex );
2641
2642 const TColStd_ListOfInteger& indices = aBSBHoles.Compare( shapeBbox );
2643
2644 for( const Standard_Integer& index : indices )
2645 holelist.Append( aHolesList[index] );
2646
2647 // Workaround for OCCT bug (https://github.com/Open-Cascade-SAS/OCCT/issues/506)
2648 // Bnd_BoundSortBox::Compare can fail to detect intersections in certain edge
2649 // cases (e.g., single item). Fall back to direct bounding box intersection
2650 // checks when Compare returns empty but intersections may exist.
2651 if( holelist.IsEmpty() )
2652 {
2653 for( size_t i = 0; i < aHoleBoxes.size(); i++ )
2654 {
2655 if( !shapeBbox.IsOut( aHoleBoxes[i] ) )
2656 holelist.Append( aHolesList[i] );
2657 }
2658 }
2659 }
2660
2661 if( holelist.IsEmpty() )
2662 return; // nothing to cut for this shape
2663
2664 TopTools_ListOfShape cutArgs;
2665 cutArgs.Append( shape );
2666
2667 BRepAlgoAPI_Cut cut;
2668
2669 cut.SetRunParallel( true );
2670 cut.SetToFillHistory( false );
2671
2672 cut.SetArguments( cutArgs );
2673 cut.SetTools( holelist );
2674 cut.Build();
2675
2676 if( cut.HasErrors() || cut.HasWarnings() )
2677 {
2678 m_reporter->Report( wxString::Format( _( "** Got problems while cutting "
2679 "%s net '%s' **" ),
2680 aWhat,
2681 UnescapeString( netname ) ),
2683 shapeBbox.Dump();
2684
2685 if( cut.HasErrors() )
2686 {
2687 wxString msg = _( "Errors:\n" );
2688 wxStringOutputStream os_stream( &msg );
2689 wxStdOutputStream out( os_stream );
2690
2691 cut.DumpErrors( out );
2692 m_reporter->Report( msg, RPT_SEVERITY_WARNING);
2693 }
2694
2695 if( cut.HasWarnings() )
2696 {
2697 wxString msg = _( "Warnings:\n" );
2698 wxStringOutputStream os_stream( &msg );
2699 wxStdOutputStream out( os_stream );
2700
2701 cut.DumpWarnings( out );
2702 m_reporter->Report( msg, RPT_SEVERITY_WARNING );
2703 }
2704 }
2705
2706 shape = cut.Shape();
2707 };
2708
2709 tp.submit_loop( 0, vec.size(), subtractLoopFn ).wait();
2710 }
2711 };
2712
2713 auto subtractShapes =
2714 [subtractShapesMap]( const wxString& aWhat, std::vector<TopoDS_Shape>& aShapesList,
2715 std::vector<TopoDS_Shape>& aHolesList, Bnd_BoundSortBox& aBSBHoles,
2716 const std::vector<Bnd_Box>& aHoleBoxes )
2717 {
2718 std::map<wxString, std::vector<TopoDS_Shape>> aShapesMap{ { wxEmptyString, aShapesList } };
2719
2720 subtractShapesMap( aWhat, aShapesMap, aHolesList, aBSBHoles, aHoleBoxes );
2721 aShapesList = aShapesMap[wxEmptyString];
2722 };
2723
2724
2725 if( m_boardCutouts.size() )
2726 {
2727 Bnd_BoundSortBox bsbHoles;
2728 std::vector<Bnd_Box> holeBoxes;
2729 buildBSB( m_boardCutouts, bsbHoles, holeBoxes );
2730
2731 subtractShapes( _( "shapes" ), m_board_outlines, m_boardCutouts, bsbHoles, holeBoxes );
2732 }
2733
2734 if( m_copperCutouts.size() )
2735 {
2736 Bnd_BoundSortBox bsbHoles;
2737 std::vector<Bnd_Box> holeBoxes;
2738 buildBSB( m_copperCutouts, bsbHoles, holeBoxes );
2739
2740 subtractShapesMap( _( "pads" ), m_board_copper_pads, m_copperCutouts, bsbHoles, holeBoxes );
2741 subtractShapesMap( _( "vias" ), m_board_copper_vias, m_copperCutouts, bsbHoles, holeBoxes );
2742 }
2743
2744 if( m_fuseShapes )
2745 {
2746 std::map<wxString, TopTools_ListOfShape> shapesToFuseMap;
2747
2748 auto addShapes = [&shapesToFuseMap]( const wxString& aNetname,
2749 const std::vector<TopoDS_Shape>& aShapes )
2750 {
2751 for( const TopoDS_Shape& shape : aShapes )
2752 shapesToFuseMap[aNetname].Append( shape );
2753 };
2754
2755 for( const auto& [netname, shapes] : m_board_copper )
2756 addShapes( netname, shapes );
2757
2758 for( const auto& [netname, shapes] : m_board_copper_pads )
2759 addShapes( netname, shapes );
2760
2761 for( const auto& [netname, shapes] : m_board_copper_vias )
2762 addShapes( netname, shapes );
2763
2764 m_reporter->Report( wxT( "Fusing shapes" ), RPT_SEVERITY_DEBUG );
2765
2766 // Do fusing in parallel
2767 std::mutex mutex;
2768
2769 auto fuseLoopFn = [&]( const wxString& aNetname )
2770 {
2771 auto& toFuse = shapesToFuseMap[aNetname];
2772 TopoDS_Shape fusedShape = fuseShapesOrCompound( toFuse, m_reporter );
2773
2774 if( !fusedShape.IsNull() )
2775 {
2776 std::unique_lock lock( mutex );
2777
2778 m_board_copper_fused[aNetname].emplace_back( fusedShape );
2779
2780 m_board_copper[aNetname].clear();
2781 m_board_copper_pads[aNetname].clear();
2782 m_board_copper_vias[aNetname].clear();
2783 }
2784 };
2785
2786 BS::multi_future<void> mf;
2787
2788 for( const auto& [netname, _] : shapesToFuseMap )
2789 mf.push_back( tp.submit_task( [&, netname]() { fuseLoopFn( netname ); } ) );
2790
2791 mf.wait();
2792 }
2793
2794 // push the board to the data structure
2795 m_reporter->Report( wxT( "Generate board full shape." ), RPT_SEVERITY_DEBUG );
2796
2797 // AddComponent adds a label that has a reference (not a parent/child relation) to the real
2798 // label. We need to extract that real label to name it for the STEP output cleanly
2799 // Why are we trying to name the bare board? Because CAD tools like SolidWorks do fun things
2800 // like "deduplicate" imported STEPs by swapping STEP assembly components with already
2801 // identically named assemblies. So we want to avoid having the PCB be generally defaulted
2802 // to "Component" or "Assembly".
2803
2804 // aCompoundNets will place all geometry within a net into one compound.
2805 // aCompoundAll will place all geometry into one compound.
2806 auto pushToAssemblyMap =
2807 [&]( const std::map<wxString, std::vector<TopoDS_Shape>>& aShapesMap,
2808 const TDF_Label& aVisMatLabel, const wxString& aShapeName, bool aCompoundNets,
2809 bool aCompoundAll, const wxString& aNiceName )
2810 {
2811 std::map<wxString, std::vector<TopoDS_Shape>> shapesMap;
2812
2813 if( aCompoundAll )
2814 {
2815 std::vector<TopoDS_Shape> allShapes;
2816
2817 for( const auto& [netname, shapesList] : aShapesMap )
2818 allShapes.insert( allShapes.end(), shapesList.begin(), shapesList.end() );
2819
2820 if( !allShapes.empty() )
2821 shapesMap[wxEmptyString].emplace_back( makeCompound( allShapes ) );
2822 }
2823 else
2824 {
2825 shapesMap = aShapesMap;
2826 }
2827
2828 for( const auto& [netname, shapesList] : shapesMap )
2829 {
2830 std::vector<TopoDS_Shape> newList;
2831
2832 if( aCompoundNets )
2833 newList.emplace_back( makeCompound( shapesList ) );
2834 else
2835 newList = shapesList;
2836
2837 int i = 1;
2838
2839 for( TopoDS_Shape& shape : newList )
2840 {
2841 Handle( TDataStd_TreeNode ) node;
2842
2843 // Dont expand the component or else coloring it gets hard
2844 TDF_Label lbl = m_assy->AddComponent( m_assy_label, shape, false );
2845 KICAD3D_INFO::Set( lbl, KICAD3D_MODEL_TYPE::BOARD, aNiceName.ToStdString() );
2846 m_pcb_labels.push_back( lbl );
2847
2848 if( m_pcb_labels.back().IsNull() )
2849 return;
2850
2851 lbl.FindAttribute( XCAFDoc::ShapeRefGUID(), node );
2852 TDF_Label shpLbl = node->Father()->Label();
2853
2854 if( !shpLbl.IsNull() )
2855 {
2856 if( visMatTool && !aVisMatLabel.IsNull() )
2857 visMatTool->SetShapeMaterial( shpLbl, aVisMatLabel );
2858
2859 wxString shapeName;
2860
2861 shapeName << m_pcbName;
2862 shapeName << '_';
2863 shapeName << aShapeName;
2864
2865 if( !netname.empty() )
2866 {
2867 shapeName << '_';
2868 shapeName << netname;
2869 }
2870
2871 if( newList.size() > 1 )
2872 {
2873 shapeName << '_';
2874 shapeName << i;
2875 }
2876
2877 TCollection_ExtendedString partname( shapeName.ToUTF8().data() );
2878 TDataStd_Name::Set( shpLbl, partname );
2879 }
2880
2881 i++;
2882 }
2883 }
2884 };
2885
2886 auto pushToAssembly =
2887 [&]( const std::vector<TopoDS_Shape>& aShapesList, const TDF_Label& aVisMatLabel,
2888 const wxString& aShapeName, bool aCompound, const wxString& aNiceName )
2889 {
2890 const std::map<wxString, std::vector<TopoDS_Shape>> shapesMap{ { wxEmptyString, aShapesList } };
2891
2892 pushToAssemblyMap( shapesMap, aVisMatLabel, aShapeName, aCompound, aCompound, aNiceName );
2893 };
2894
2895 auto makeMaterial =
2896 [&]( const TCollection_AsciiString& aName, const Quantity_ColorRGBA& aBaseColor,
2897 double aMetallic, double aRoughness ) -> TDF_Label
2898 {
2899 Handle( XCAFDoc_VisMaterial ) vismat = new XCAFDoc_VisMaterial;
2900 XCAFDoc_VisMaterialPBR pbr;
2901 pbr.BaseColor = aBaseColor;
2902 pbr.Metallic = aMetallic;
2903 pbr.Roughness = aRoughness;
2904 vismat->SetPbrMaterial( pbr );
2905 return visMatTool->AddMaterial( vismat, aName );
2906 };
2907
2908 // Init colors for the board items
2909 Quantity_ColorRGBA copper_color( m_copperColor[0], m_copperColor[1], m_copperColor[2], 1.0 );
2910 Quantity_ColorRGBA pad_color( m_padColor[0], m_padColor[1], m_padColor[2], 1.0 );
2911
2912 Quantity_ColorRGBA board_color( 0.42f, 0.45f, 0.29f, 0.98f );
2913 Quantity_ColorRGBA front_silk_color( 1.0f, 1.0f, 1.0f, 0.9f );
2914 Quantity_ColorRGBA back_silk_color = front_silk_color;
2915 Quantity_ColorRGBA front_mask_color( 0.08f, 0.2f, 0.14f, 0.83f );
2916 Quantity_ColorRGBA back_mask_color = front_mask_color;
2917
2918 // Get colors from stackup
2919 for( const BOARD_STACKUP_ITEM* item : m_stackup.GetList() )
2920 {
2921 COLOR4D col;
2922
2923 if( !colorFromStackup( item->GetType(), item->GetColor(), col ) )
2924 continue;
2925
2926 if( item->GetBrdLayerId() == F_Mask || item->GetBrdLayerId() == B_Mask )
2927 {
2928 col.Darken( 0.2 );
2929
2930 if( item->GetBrdLayerId() == F_Mask )
2931 front_mask_color.SetValues( col.r, col.g, col.b, col.a );
2932 else
2933 back_mask_color.SetValues( col.r, col.g, col.b, col.a );
2934 }
2935
2936 if( item->GetBrdLayerId() == F_SilkS )
2937 front_silk_color.SetValues( col.r, col.g, col.b, col.a );
2938 else if( item->GetBrdLayerId() == B_SilkS )
2939 back_silk_color.SetValues( col.r, col.g, col.b, col.a );
2940
2941 if( item->GetType() == BS_ITEM_TYPE_DIELECTRIC && item->GetTypeName() == KEY_CORE )
2942 board_color.SetValues( col.r, col.g, col.b, col.a );
2943 }
2944
2945 // Paint board body in soldermask colors if soldermask is not exported as a layer
2946 if( !m_enabledLayers.Contains( F_Mask ) && !m_enabledLayers.Contains( B_Mask ) )
2947 {
2948 board_color = front_mask_color;
2949 board_color.SetAlpha( 1.0 );
2950 }
2951
2952 TDF_Label front_mask_mat = makeMaterial( "soldermask", front_mask_color, 0.0, 0.6 );
2953 TDF_Label back_mask_mat = makeMaterial( "soldermask", back_mask_color, 0.0, 0.6 );
2954 TDF_Label front_silk_mat = makeMaterial( "silkscreen", front_silk_color, 0.0, 0.9 );
2955 TDF_Label back_silk_mat = makeMaterial( "silkscreen", back_silk_color, 0.0, 0.9 );
2956 TDF_Label copper_mat = makeMaterial( "copper", copper_color, 1.0, 0.4 );
2957 TDF_Label pad_mat = makeMaterial( "pad", pad_color, 1.0, 0.4 );
2958 TDF_Label board_mat = makeMaterial( "board", board_color, 0.0, 0.8 );
2959
2960 pushToAssemblyMap( m_board_copper, copper_mat, "copper", true, true, "Copper" );
2961 pushToAssemblyMap( m_board_copper_pads, pad_mat, "pad", true, true, "Pads" );
2962 pushToAssemblyMap( m_board_copper_vias, copper_mat, "via", true, true, "Via" );
2963 pushToAssemblyMap( m_board_copper_fused, copper_mat, "copper", true, true, "Copper" );
2964 pushToAssembly( m_board_front_silk, front_silk_mat, "silkscreen", true, "Top Silkscreen" );
2965 pushToAssembly( m_board_back_silk, back_silk_mat, "silkscreen", true, "Bottom Silkscreen" );
2966 pushToAssembly( m_board_front_mask, front_mask_mat, "soldermask", true, "Top Soldermask" );
2967 pushToAssembly( m_board_back_mask, back_mask_mat, "soldermask", true, "Bottom Soldermask" );
2968
2969 if( aPushBoardBody )
2970 pushToAssembly( m_board_outlines, board_mat, "PCB", false, "Body" );
2971
2972 Quantity_ColorRGBA pinClr( Quantity_Color( 0.75, 0.75, 0.75, Quantity_TOC_RGB ), 1.0 );
2973 TDF_Label pin_mat = makeMaterial( "extruded_pin", pinClr, 0.6, 0.3 );
2974
2975 for( auto& entry : m_extruded_bodies )
2976 {
2977 if( entry.bodyShapes.empty() && entry.pinShapes.empty() )
2978 continue;
2979
2980 TopoDS_Compound asmCompound;
2981 BRep_Builder asmBuilder;
2982 asmBuilder.MakeCompound( asmCompound );
2983 TDF_Label fpLabel = m_assy->AddShape( asmCompound, true );
2984 TDataStd_Name::Set( fpLabel, TCollection_ExtendedString( ( entry.refDes + " (extruded)" ).ToUTF8().data() ) );
2985
2986 if( !entry.bodyShapes.empty() )
2987 {
2988 double r = ( ( entry.colorKey >> 24 ) & 0xFF ) / 255.0;
2989 double g = ( ( entry.colorKey >> 16 ) & 0xFF ) / 255.0;
2990 double b = ( ( entry.colorKey >> 8 ) & 0xFF ) / 255.0;
2991 double a = ( entry.colorKey & 0xFF ) / 255.0;
2992
2993 double metallic, roughness;
2994
2995 switch( entry.material )
2996 {
2997 default:
2999 metallic = 0.0;
3000 roughness = 0.6;
3001 break;
3003 metallic = 0.0;
3004 roughness = 0.9;
3005 break;
3007 metallic = 0.8;
3008 roughness = 0.3;
3009 break;
3011 metallic = 1.0;
3012 roughness = 0.4;
3013 break;
3014 }
3015
3016 Quantity_ColorRGBA bodyClr( Quantity_Color( r, g, b, Quantity_TOC_RGB ), a );
3017 TDF_Label body_mat = makeMaterial( "extruded_body", bodyClr, metallic, roughness );
3018
3019 TopoDS_Shape bodyCompound = makeCompound( entry.bodyShapes );
3020 TDF_Label bodyLbl = m_assy->AddComponent( fpLabel, bodyCompound, false );
3021
3022 Handle( TDataStd_TreeNode ) bodyNode;
3023 bodyLbl.FindAttribute( XCAFDoc::ShapeRefGUID(), bodyNode );
3024 TDF_Label bodyShpLbl = bodyNode->Father()->Label();
3025
3026 if( !bodyShpLbl.IsNull() )
3027 {
3028 visMatTool->SetShapeMaterial( bodyShpLbl, body_mat );
3029 TDataStd_Name::Set( bodyShpLbl,
3030 TCollection_ExtendedString( ( entry.refDes + "_body" ).ToUTF8().data() ) );
3031 }
3032 }
3033
3034 int pinIdx = 1;
3035
3036 for( TopoDS_Shape& pinShape : entry.pinShapes )
3037 {
3038 TDF_Label pinLbl = m_assy->AddComponent( fpLabel, pinShape, false );
3039
3040 Handle( TDataStd_TreeNode ) pinNode;
3041 pinLbl.FindAttribute( XCAFDoc::ShapeRefGUID(), pinNode );
3042 TDF_Label pinShpLbl = pinNode->Father()->Label();
3043
3044 if( !pinShpLbl.IsNull() )
3045 {
3046 visMatTool->SetShapeMaterial( pinShpLbl, pin_mat );
3047 wxString pinName = wxString::Format( "%s_pin_%d", entry.refDes, pinIdx++ );
3048 TDataStd_Name::Set( pinShpLbl, TCollection_ExtendedString( pinName.ToUTF8().data() ) );
3049 }
3050 }
3051
3052 TopLoc_Location loc;
3053 TDF_Label fpCompLbl = m_assy->AddComponent( m_assy_label, fpLabel, loc );
3054 TDataStd_Name::Set( fpCompLbl, TCollection_ExtendedString( entry.refDes.ToUTF8().data() ) );
3055 KICAD3D_INFO::Set( fpCompLbl, KICAD3D_MODEL_TYPE::BOARD, entry.refDes.ToStdString() );
3056 m_pcb_labels.push_back( fpCompLbl );
3057 }
3058
3059#if( defined OCC_VERSION_HEX ) && ( OCC_VERSION_HEX > 0x070101 )
3060 m_assy->UpdateAssemblies();
3061#endif
3062
3063 return true;
3064}
3065
3066
3067#ifdef SUPPORTS_IGES
3068// write the assembly model in IGES format
3069bool STEP_PCB_MODEL::WriteIGES( const wxString& aFileName )
3070{
3071 if( !isBoardOutlineValid() )
3072 {
3073 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
3074 aFileName ),
3076 return false;
3077 }
3078
3080
3081 wxFileName fn( aFileName );
3082 IGESControl_Controller::Init();
3083 IGESCAFControl_Writer writer;
3084 writer.SetColorMode( Standard_True );
3085 writer.SetNameMode( Standard_True );
3086 IGESData_GlobalSection header = writer.Model()->GlobalSection();
3087 header.SetFileName( new TCollection_HAsciiString( fn.GetFullName().ToAscii() ) );
3088 header.SetSendName( new TCollection_HAsciiString( "KiCad electronic assembly" ) );
3089 header.SetAuthorName( new TCollection_HAsciiString( Interface_Static::CVal( "write.iges.header.author" ) ) );
3090 header.SetCompanyName( new TCollection_HAsciiString( Interface_Static::CVal( "write.iges.header.company" ) ) );
3091 writer.Model()->SetGlobalSection( header );
3092
3093 if( Standard_False == writer.Perform( m_doc, aFileName.c_str() ) )
3094 return false;
3095
3096 return true;
3097}
3098#endif
3099
3100bool STEP_PCB_MODEL::CompressSTEP( wxString& inputFile, wxString& outputFile )
3101{
3102 wxFileInputStream input( inputFile );
3103 wxFileOutputStream output( outputFile );
3104
3105 if( !input.IsOk() )
3106 {
3107 m_reporter->Report( wxString::Format( _( "Cannot create input stream '%s'.\n" ), inputFile ) );
3108 return false;
3109 }
3110
3111 if( !output.IsOk() )
3112 {
3113 m_reporter->Report( wxString::Format( _( "Cannot create output stream '%s'.\n" ), outputFile ) );
3114 return false;
3115 }
3116
3117 wxZlibOutputStream zlibStream( output, -1, wxZLIB_GZIP );
3118
3119 if( !zlibStream.IsOk() )
3120 {
3121 m_reporter->Report( _( "Impossible create compress stream" ) );
3122 return false;
3123 }
3124
3125 input.Read( zlibStream );
3126
3127 if( input.LastRead() == 0 || zlibStream.LastWrite() == 0 )
3128 {
3129 m_reporter->Report( _( "Compress read or write error" ) );
3130 return false;
3131 }
3132
3133 zlibStream.Close();
3134 output.Close();
3135
3136 return true;
3137}
3138
3139bool STEP_PCB_MODEL::WriteSTEP( const wxString& aFileName, bool aOptimize, bool compress )
3140{
3141 if( !isBoardOutlineValid() )
3142 {
3143 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
3144 aFileName ),
3146 return false;
3147 }
3148
3150
3151 wxFileName fn( aFileName );
3152
3153 STEPCAFControl_Writer writer;
3154 writer.SetColorMode( Standard_True );
3155 writer.SetNameMode( Standard_True );
3156
3157 // This must be set before we "transfer" the document.
3158 // Should default to kicad_pcb.general.title_block.title,
3159 // but in the meantime, defaulting to the basename of the output
3160 // target is still better than "open cascade step translter v..."
3161 // UTF8 should be ok from ISO 10303-21:2016, but... older stuff? use boring ascii
3162 if( !Interface_Static::SetCVal( "write.step.product.name", fn.GetName().ToAscii() ) )
3163 {
3164 m_reporter->Report( _( "Failed to set STEP product name, but will attempt to continue." ),
3166 }
3167
3168 // Setting write.surfacecurve.mode to 0 reduces file size and write/read times.
3169 // But there are reports that this mode might be less compatible in some cases.
3170 if( !Interface_Static::SetIVal( "write.surfacecurve.mode", aOptimize ? 0 : 1 ) )
3171 {
3172 m_reporter->Report( _( "Failed to set surface curve mode, but will attempt to continue." ),
3174 }
3175
3176 if( Standard_False == writer.Transfer( m_doc, STEPControl_AsIs ) )
3177 return false;
3178
3179 APIHeaderSection_MakeHeader hdr( writer.ChangeWriter().Model() );
3180
3181 // Note: use only Ascii7 chars, non Ascii7 chars (therefore UFT8 chars)
3182 // are creating issues in the step file
3183 hdr.SetName( new TCollection_HAsciiString( fn.GetFullName().ToAscii() ) );
3184
3185 // TODO: how to control and ensure consistency with IGES?
3186 hdr.SetAuthorValue( 1, new TCollection_HAsciiString( "Pcbnew" ) );
3187 hdr.SetOrganizationValue( 1, new TCollection_HAsciiString( "Kicad" ) );
3188 hdr.SetOriginatingSystem( new TCollection_HAsciiString( "KiCad to STEP converter" ) );
3189 hdr.SetDescriptionValue( 1, new TCollection_HAsciiString( "KiCad electronic assembly" ) );
3190
3191 bool success = true;
3192
3193 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
3194 wxString currCWD = wxGetCwd();
3195 wxString workCWD = fn.GetPath();
3196
3197 if( !workCWD.IsEmpty() )
3198 wxSetWorkingDirectory( workCWD );
3199
3200 wxString tmpfname( "$tempfile$.step" );
3201
3202 if( Standard_False == writer.Write( tmpfname.c_str() ) )
3203 success = false;
3204
3205 if( compress && success )
3206 {
3207 wxString srcTmp( tmpfname );
3208 wxString dstTmp( "$tempfile$.stpz" );
3209
3210 success = STEP_PCB_MODEL::CompressSTEP( srcTmp, dstTmp );
3211 wxRemoveFile( srcTmp );
3212
3213 tmpfname = dstTmp;
3214 }
3215
3216 if( success )
3217 {
3218
3219 // Preserve the permissions of the current file
3220 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpfname.c_str() );
3221
3222 if( !wxRenameFile( tmpfname, fn.GetFullName(), true ) )
3223 {
3224 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'." ),
3225 tmpfname,
3226 fn.GetFullName() ),
3228 success = false;
3229 }
3230 }
3231
3232 wxSetWorkingDirectory( currCWD );
3233
3234 return success;
3235}
3236
3237
3238bool STEP_PCB_MODEL::WriteBREP( const wxString& aFileName )
3239{
3240 if( !isBoardOutlineValid() )
3241 {
3242 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
3243 aFileName ),
3245 return false;
3246 }
3247
3249
3250 // s_assy = shape tool for the source
3251 Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
3252
3253 // retrieve assembly as a single shape
3254 TopoDS_Shape shape = getOneShape( s_assy );
3255
3256 wxFileName fn( aFileName );
3257
3258 wxFFileOutputStream ffStream( fn.GetFullPath() );
3259 wxStdOutputStream stdStream( ffStream );
3260
3261#if OCC_VERSION_HEX >= 0x070600
3262 BRepTools::Write( shape, stdStream, false, false, TopTools_FormatVersion_VERSION_1 );
3263#else
3264 BRepTools::Write( shape, stdStream );
3265#endif
3266
3267 return true;
3268}
3269
3270
3271bool STEP_PCB_MODEL::WriteXAO( const wxString& aFileName )
3272{
3273 wxFileName fn( aFileName );
3274
3275 wxFFileOutputStream ffStream( fn.GetFullPath() );
3276 wxStdOutputStream file( ffStream );
3277
3278 if( !ffStream.IsOk() )
3279 {
3280 m_reporter->Report( wxString::Format( "Could not open file '%s'", fn.GetFullPath() ),
3282 return false;
3283 }
3284
3286
3287 // s_assy = shape tool for the source
3288 Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
3289
3290 // retrieve assembly as a single shape
3291 const TopoDS_Shape shape = getOneShape( s_assy );
3292
3293 std::map<wxString, std::vector<int>> groups[4];
3294 std::map<wxString, double> groupAreas;
3295 TopExp_Explorer exp;
3296 int faceIndex = 0;
3297
3298 for( exp.Init( shape, TopAbs_FACE ); exp.More(); exp.Next() )
3299 {
3300 TopoDS_Shape subShape = exp.Current();
3301
3302 Bnd_Box bbox;
3303 BRepBndLib::Add( subShape, bbox );
3304
3305 for( const auto& [padKey, pairs] : m_pad_points )
3306 {
3307 for( const auto& pair : pairs )
3308 {
3309 const auto& [point, padTestShape] = pair;
3310
3311 if( bbox.IsOut( point ) )
3312 continue;
3313
3314 BRepAdaptor_Surface surface( TopoDS::Face( subShape ) );
3315
3316 if( surface.GetType() != GeomAbs_Plane )
3317 continue;
3318
3319 BRepExtrema_DistShapeShape dist( padTestShape, subShape );
3320 dist.Perform();
3321
3322 if( !dist.IsDone() )
3323 continue;
3324
3325 if( dist.Value() < Precision::Approximation() )
3326 {
3327 // Push as a face group
3328 groups[2][padKey].push_back( faceIndex );
3329
3330 GProp_GProps system;
3331 BRepGProp::SurfaceProperties( subShape, system );
3332
3333 double surfaceArea = system.Mass() / 1e6; // Convert to meters^2
3334 groupAreas[padKey] += surfaceArea;
3335 }
3336 }
3337 }
3338
3339 faceIndex++;
3340 }
3341
3342 // Based on Gmsh code
3343 file << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << std::endl;
3344 file << "<XAO version=\"1.0\" author=\"KiCad\">" << std::endl;
3345 file << " <geometry name=\"" << fn.GetName() << "\">" << std::endl;
3346 file << " <shape format=\"BREP\"><![CDATA[";
3347#if OCC_VERSION_HEX < 0x070600
3348 BRepTools::Write( shape, file );
3349#else
3350 BRepTools::Write( shape, file, Standard_True, Standard_True, TopTools_FormatVersion_VERSION_1 );
3351#endif
3352 file << "]]></shape>" << std::endl;
3353 file << " <topology>" << std::endl;
3354
3355 TopTools_IndexedMapOfShape mainMap;
3356 TopExp::MapShapes( shape, mainMap );
3357 std::set<int> topo[4];
3358
3359 static const TopAbs_ShapeEnum c_dimShapeTypes[] = { TopAbs_VERTEX, TopAbs_EDGE, TopAbs_FACE,
3360 TopAbs_SOLID };
3361
3362 static const std::string c_dimLabel[] = { "vertex", "edge", "face", "solid" };
3363 static const std::string c_dimLabels[] = { "vertices", "edges", "faces", "solids" };
3364
3365 for( int dim = 0; dim < 4; dim++ )
3366 {
3367 for( exp.Init( shape, c_dimShapeTypes[dim] ); exp.More(); exp.Next() )
3368 {
3369 TopoDS_Shape subShape = exp.Current();
3370 int idx = mainMap.FindIndex( subShape );
3371
3372 if( idx && !topo[dim].count( idx ) )
3373 topo[dim].insert( idx );
3374 }
3375 }
3376
3377 for( int dim = 0; dim <= 3; dim++ )
3378 {
3379 std::string labels = c_dimLabels[dim];
3380 std::string label = c_dimLabel[dim];
3381
3382 file << " <" << labels << " count=\"" << topo[dim].size() << "\">" << std::endl;
3383 int index = 0;
3384
3385 for( auto p : topo[dim] )
3386 {
3387 std::string name( "" );
3388 file << " <" << label << " index=\"" << index << "\" "
3389 << "name=\"" << name << "\" "
3390 << "reference=\"" << p << "\"/>" << std::endl;
3391
3392 index++;
3393 }
3394 file << " </" << labels << ">" << std::endl;
3395 }
3396
3397 file << " </topology>" << std::endl;
3398 file << " </geometry>" << std::endl;
3399 file << " <groups count=\""
3400 << groups[0].size() + groups[1].size() + groups[2].size() + groups[3].size() << "\">"
3401 << std::endl;
3402
3403 int groupNumber = 1;
3404
3405 m_reporter->Report( wxT( "Pad definitions:" ), RPT_SEVERITY_DEBUG );
3406 m_reporter->Report( wxT( "Number\tName\tArea (m^2)" ), RPT_SEVERITY_DEBUG );
3407
3408 for( int dim = 0; dim <= 3; dim++ )
3409 {
3410 std::string label = c_dimLabel[dim];
3411
3412 for( auto g : groups[dim] )
3413 {
3414 //std::string name = model->getPhysicalName( dim, g.first );
3415 wxString name = g.first;
3416
3417 if( name.empty() )
3418 { // create same unique name as for MED export
3419 std::ostringstream gs;
3420 gs << "G_" << dim << "D_" << g.first;
3421 name = gs.str();
3422 }
3423 file << " <group name=\"" << name << "\" dimension=\"" << label;
3424//#if 1
3425// // Gmsh XAO extension: also save the physical tag, so that XAO can be used
3426// // to serialize OCC geometries, ready to be used by GetDP, GmshFEM & co
3427// file << "\" tag=\"" << g.first;
3428//#endif
3429 file << "\" count=\"" << g.second.size() << "\">" << std::endl;
3430
3431 for( auto index : g.second )
3432 file << " <element index=\"" << index << "\"/>" << std::endl;
3433
3434 file << " </group>" << std::endl;
3435
3436 m_reporter->Report( wxString::Format( "%d\t%s\t%g",
3437 groupNumber,
3438 name,
3439 groupAreas[name] ),
3441
3442 groupNumber++;
3443 }
3444 }
3445
3446 m_reporter->Report( wxT( "" ), RPT_SEVERITY_DEBUG );
3447
3448 file << " </groups>" << std::endl;
3449 file << " <fields count=\"0\"/>" << std::endl;
3450 file << "</XAO>" << std::endl;
3451
3452 return true;
3453}
3454
3455
3456bool STEP_PCB_MODEL::getModelLabel( const wxString& aBaseName, const wxString& aFileName,
3457 const std::vector<wxString>& aAltFilenames, VECTOR3D aScale,
3458 TDF_Label& aLabel, bool aSubstituteModels,
3459 wxString* aErrorMessage )
3460{
3461 std::string fileNameUTF8 = aFileName.utf8_string();
3462
3463 std::string model_key = fileNameUTF8 + "_" + std::to_string( aScale.x ) + "_"
3464 + std::to_string( aScale.y ) + "_" + std::to_string( aScale.z );
3465
3466 MODEL_MAP::const_iterator mm = m_models.find( model_key );
3467
3468 if( mm != m_models.end() )
3469 {
3470 aLabel = mm->second;
3471 return true;
3472 }
3473
3474 aLabel.Nullify();
3475
3476 Handle( TDocStd_Document ) doc;
3477 m_app->NewDocument( "MDTV-XCAF", doc );
3478
3479 MODEL3D_FORMAT_TYPE modelFmt = fileType( fileNameUTF8.c_str() );
3480 TCollection_ExtendedString partname( aBaseName.utf8_str() );
3481
3482 switch( modelFmt )
3483 {
3484 case FMT_IGES:
3485 if( !readIGES( doc, fileNameUTF8.c_str() ) )
3486 {
3487 m_reporter->Report( wxString::Format( wxT( "readIGES() failed on filename '%s'." ), aFileName ),
3489 return false;
3490 }
3491
3492 break;
3493
3494 case FMT_STEP:
3495 if( !readSTEP( doc, fileNameUTF8.c_str() ) )
3496 {
3497 m_reporter->Report( wxString::Format( wxT( "readSTEP() failed on filename '%s'." ), aFileName ),
3499 return false;
3500 }
3501
3502 break;
3503
3504 case FMT_STEPZ:
3505 {
3506 // To export a compressed step file (.stpz or .stp.gz file), the best way is to
3507 // decaompress it in a temporaty file and load this temporary file
3508 wxFFileInputStream ifile( aFileName );
3509 wxFileName outFile( aFileName );
3510
3511 outFile.SetPath( wxStandardPaths::Get().GetTempDir() );
3512 outFile.SetExt( wxT( "step" ) );
3513 wxFileOffset size = ifile.GetLength();
3514
3515 if( size == wxInvalidOffset )
3516 {
3517 m_reporter->Report( wxString::Format( wxT( "getModelLabel() failed on filename '%s'." ),
3518 aFileName ),
3520 return false;
3521 }
3522
3523 {
3524 bool success = false;
3525
3526 {
3527 wxFFileOutputStream ofile( outFile.GetFullPath() );
3528
3529 if( !ofile.IsOk() )
3530 return false;
3531
3532 char* buffer = new char[size];
3533
3534 ifile.Read( buffer, size );
3535 std::string expanded;
3536
3537 try
3538 {
3539 expanded = gzip::decompress( buffer, size );
3540 success = true;
3541 }
3542 catch( ... )
3543 {
3544 // ignore - we try unzipping it below
3545 }
3546
3547 if( expanded.empty() )
3548 {
3549 ifile.Reset();
3550 ifile.SeekI( 0 );
3551 wxZipInputStream izipfile( ifile );
3552 std::unique_ptr<wxZipEntry> zip_file( izipfile.GetNextEntry() );
3553
3554 if( zip_file && !zip_file->IsDir() && izipfile.CanRead() )
3555 {
3556 izipfile.Read( ofile );
3557 success = true;
3558 }
3559 else
3560 {
3561 m_reporter->Report( wxString::Format( wxT( "failed to decompress '%s'." ), aFileName ),
3563 }
3564 }
3565 else
3566 {
3567 ofile.Write( expanded.data(), expanded.size() );
3568 }
3569
3570 delete[] buffer;
3571 }
3572
3573 if( success )
3574 {
3575 success = getModelLabel( aBaseName, outFile.GetFullPath(), aAltFilenames,
3576 VECTOR3D( 1.0, 1.0, 1.0 ), aLabel, false );
3577 }
3578
3579 return success;
3580 }
3581
3582 break;
3583 }
3584
3585 case FMT_WRL:
3586 case FMT_WRZ:
3587 /* WRL files are preferred for internal rendering, due to superior material properties, etc.
3588 * However they are not suitable for MCAD export.
3589 *
3590 * If a .wrl file is specified, attempt to locate a replacement file for it.
3591 *
3592 * If a valid replacement file is found, the label for THAT file will be associated with
3593 * the .wrl file
3594 */
3595 if( aSubstituteModels )
3596 {
3597 wxFileName wrlName( aFileName );
3598
3599 wxString basePath = wrlName.GetPath();
3600 wxString baseName = wrlName.GetName();
3601
3602 // List of alternate files to look for
3603 // Given in order of preference
3604 // (Break if match is found)
3605 wxArrayString alts;
3606
3607 // Step files
3608 alts.Add( wxT( "stp" ) );
3609 alts.Add( wxT( "step" ) );
3610 alts.Add( wxT( "STP" ) );
3611 alts.Add( wxT( "STEP" ) );
3612 alts.Add( wxT( "Stp" ) );
3613 alts.Add( wxT( "Step" ) );
3614 alts.Add( wxT( "stpz" ) );
3615 alts.Add( wxT( "stpZ" ) );
3616 alts.Add( wxT( "STPZ" ) );
3617 alts.Add( wxT( "step.gz" ) );
3618 alts.Add( wxT( "stp.gz" ) );
3619
3620 // IGES files
3621 alts.Add( wxT( "iges" ) );
3622 alts.Add( wxT( "IGES" ) );
3623 alts.Add( wxT( "igs" ) );
3624 alts.Add( wxT( "IGS" ) );
3625
3626 //TODO - Other alternative formats?
3627
3628 for( const auto& altExt : alts )
3629 {
3630 wxFileName altFile;
3631
3632 if( !aAltFilenames.empty() )
3633 {
3634 for( const wxString& altPath : aAltFilenames )
3635 {
3636 wxFileName iterFn( altPath );
3637
3638 if( iterFn.GetExt() == altExt )
3639 {
3640 altFile = iterFn;
3641 break;
3642 }
3643 }
3644 }
3645 else
3646 {
3647 altFile = wxFileName( basePath, baseName + wxT( "." ) + altExt );
3648 }
3649
3650 if( altFile.IsOk() && altFile.FileExists() )
3651 {
3652 // When substituting a STEP/IGS file for VRML, do not apply the VRML scaling
3653 // to the new STEP model. This process of auto-substitution is janky as all
3654 // heck so let's not mix up un-displayed scale factors with potentially
3655 // mis-matched files. And hope that the user doesn't have multiples files
3656 // named "model.wrl" and "model.stp" referring to different parts.
3657 // TODO: Fix model handling in v7. Default models should only be STP.
3658 // Have option to override this in DISPLAY.
3659 if( getModelLabel( aBaseName, altFile.GetFullPath(), {},
3660 VECTOR3D( 1.0, 1.0, 1.0 ), aLabel, false ) )
3661 {
3662 return true;
3663 }
3664 }
3665 }
3666 }
3667
3668 // VRML models only work when exporting to mesh formats
3669 // Also OCCT < 7.9.0 fails to load most VRML 2.0 models because of Switch nodes
3673 {
3674 if( readVRML( doc, fileNameUTF8.c_str() ) )
3675 {
3676 Handle( XCAFDoc_ShapeTool ) shapeTool =
3677 XCAFDoc_DocumentTool::ShapeTool( doc->Main() );
3678
3679 prefixNames( shapeTool->Label(), partname );
3680 }
3681 else
3682 {
3683 m_reporter->Report(
3684 wxString::Format( wxT( "readVRML() failed on filename '%s'." ),
3685 aFileName ),
3687
3688 return false;
3689 }
3690 }
3691 else // Substitution is not allowed
3692 {
3693 if( aErrorMessage )
3694 aErrorMessage->Printf( _( "Cannot use VRML models when exporting to non-mesh formats." ) );
3695
3696 return false;
3697 }
3698
3699 break;
3700
3701 // TODO: implement IDF and EMN converters
3702
3703 default:
3704 m_reporter->Report( wxString::Format( _( "Cannot identify actual file type for '%s'." ), aFileName ),
3706 return false;
3707 }
3708
3709 aLabel = transferModel( doc, m_doc, aScale );
3710
3711 if( aLabel.IsNull() )
3712 {
3713 m_reporter->Report( wxString::Format( _( "Could not transfer model data from file '%s'." ), aFileName ),
3715 return false;
3716 }
3717
3718 // attach the PART NAME ( base filename: note that in principle
3719 // different models may have the same base filename )
3720 TDataStd_Name::Set( aLabel, partname );
3721
3722 m_models.insert( MODEL_DATUM( model_key, aLabel ) );
3723 ++m_components;
3724 return true;
3725}
3726
3727
3728bool STEP_PCB_MODEL::getModelLocation( bool aBottom, const VECTOR2D& aPosition, double aRotation,
3729 const VECTOR3D& aOffset, const VECTOR3D& aOrientation,
3730 TopLoc_Location& aLocation )
3731{
3732 // Order of operations:
3733 // a. aOrientation is applied -Z*-Y*-X
3734 // b. aOffset is applied
3735 // Top ? add thickness to the Z offset
3736 // c. Bottom ? Rotate on X axis (in contrast to most ECAD which mirror on Y),
3737 // then rotate on +Z
3738 // Top ? rotate on -Z
3739 // d. aPosition is applied
3740 //
3741 // Note: Y axis is inverted in KiCad
3742
3743 gp_Trsf lPos;
3744 lPos.SetTranslation( gp_Vec( aPosition.x, -aPosition.y, 0.0 ) );
3745
3746 // Offset board thickness
3747 VECTOR3D offset( aOffset );
3748 offset.z += BOARD_OFFSET;
3749
3750 double boardThickness;
3751 double boardZPos;
3752 getBoardBodyZPlacement( boardZPos, boardThickness );
3753 double top = std::max( boardZPos, boardZPos + boardThickness );
3754 double bottom = std::min( boardZPos, boardZPos + boardThickness );
3755
3756 // 3D step models are placed on the top of copper layers.
3757 // This is true for SMD shapes, and perhaps not always true for TH shapes,
3758 // but we use this Z position for any 3D shape.
3759 double f_pos, f_thickness;
3760 getLayerZPlacement( F_Cu, f_pos, f_thickness );
3761 top += f_thickness;
3762 getLayerZPlacement( B_Cu, f_pos, f_thickness );
3763 bottom += f_thickness; // f_thickness is < 0 for B_Cu layer
3764
3765 gp_Trsf lRot;
3766
3767 if( aBottom )
3768 {
3769 offset.z -= bottom;
3770 lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), aRotation );
3771 lPos.Multiply( lRot );
3772 lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 1.0, 0.0, 0.0 ) ), M_PI );
3773 lPos.Multiply( lRot );
3774 }
3775 else
3776 {
3777 offset.z += top;
3778 lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), aRotation );
3779 lPos.Multiply( lRot );
3780 }
3781
3782 gp_Trsf lOff;
3783 lOff.SetTranslation( gp_Vec( offset.x, offset.y, offset.z ) );
3784 lPos.Multiply( lOff );
3785
3786 gp_Trsf lOrient;
3787 lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), -aOrientation.z );
3788 lPos.Multiply( lOrient );
3789 lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 1.0, 0.0 ) ), -aOrientation.y );
3790 lPos.Multiply( lOrient );
3791 lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 1.0, 0.0, 0.0 ) ), -aOrientation.x );
3792 lPos.Multiply( lOrient );
3793
3794 aLocation = TopLoc_Location( lPos );
3795 return true;
3796}
3797
3798
3799bool STEP_PCB_MODEL::readIGES( Handle( TDocStd_Document )& doc, const char* fname )
3800{
3801 IGESControl_Controller::Init();
3802 IGESCAFControl_Reader reader;
3803 IFSelect_ReturnStatus stat = reader.ReadFile( fname );
3804
3805 if( stat != IFSelect_RetDone )
3806 return false;
3807
3808 // Enable user-defined shape precision
3809 if( !Interface_Static::SetIVal( "read.precision.mode", 1 ) )
3810 return false;
3811
3812 // Set the shape conversion precision to USER_PREC (default 0.0001 has too many triangles)
3813 if( !Interface_Static::SetRVal( "read.precision.val", USER_PREC ) )
3814 return false;
3815
3816 // set other translation options
3817 reader.SetColorMode( true ); // use model colors
3818 reader.SetNameMode( false ); // don't use IGES label names
3819 reader.SetLayerMode( false ); // ignore LAYER data
3820
3821 if( !reader.Transfer( doc ) )
3822 {
3823 if( doc->CanClose() == CDM_CCS_OK )
3824 doc->Close();
3825
3826 return false;
3827 }
3828
3829 // are there any shapes to translate?
3830 if( reader.NbShapes() < 1 )
3831 {
3832 if( doc->CanClose() == CDM_CCS_OK )
3833 doc->Close();
3834
3835 return false;
3836 }
3837
3838 return true;
3839}
3840
3841
3842bool STEP_PCB_MODEL::readSTEP( Handle( TDocStd_Document )& doc, const char* fname )
3843{
3844 STEPCAFControl_Reader reader;
3845 IFSelect_ReturnStatus stat = reader.ReadFile( fname );
3846
3847 if( stat != IFSelect_RetDone )
3848 return false;
3849
3850 // Enable user-defined shape precision
3851 if( !Interface_Static::SetIVal( "read.precision.mode", 1 ) )
3852 return false;
3853
3854 // Set the shape conversion precision to USER_PREC (default 0.0001 has too many triangles)
3855 if( !Interface_Static::SetRVal( "read.precision.val", USER_PREC ) )
3856 return false;
3857
3858 // set other translation options
3859 reader.SetColorMode( true ); // use model colors
3860 reader.SetNameMode( true ); // use label names
3861 reader.SetLayerMode( false ); // ignore LAYER data
3862
3863 if( !reader.Transfer( doc ) )
3864 {
3865 if( doc->CanClose() == CDM_CCS_OK )
3866 doc->Close();
3867
3868 return false;
3869 }
3870
3871 // are there any shapes to translate?
3872 if( reader.NbRootsForTransfer() < 1 )
3873 {
3874 if( doc->CanClose() == CDM_CCS_OK )
3875 doc->Close();
3876
3877 return false;
3878 }
3879
3880 return true;
3881}
3882
3883
3884bool STEP_PCB_MODEL::readVRML( Handle( TDocStd_Document ) & doc, const char* fname )
3885{
3886#if OCC_VERSION_HEX >= 0x070700
3887 VrmlAPI_CafReader reader;
3888 RWMesh_CoordinateSystemConverter conv;
3889 conv.SetInputLengthUnit( 2.54 );
3890 reader.SetCoordinateSystemConverter( conv );
3891 reader.SetDocument( doc );
3892
3893 if( !reader.Perform( TCollection_AsciiString( fname ), Message_ProgressRange() ) )
3894 return false;
3895
3896 return true;
3897#else
3898 return false;
3899#endif
3900}
3901
3902
3903TDF_Label STEP_PCB_MODEL::transferModel( Handle( TDocStd_Document ) & source,
3904 Handle( TDocStd_Document ) & dest, const VECTOR3D& aScale )
3905{
3906 Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( source->Main() );
3907
3908 TDF_LabelSequence frshapes;
3909 s_assy->GetFreeShapes( frshapes );
3910
3911 Handle( XCAFDoc_ShapeTool ) d_assy = XCAFDoc_DocumentTool::ShapeTool( dest->Main() );
3912
3913 // Create a new top-level assembly in the destination and clone the source's free shapes
3914 // into it with XCAFDoc_Editor::Extract. Extract rebuilds the XDE label tree by walking
3915 // the component reference graph, so it preserves colors, names and sub-assembly structure
3916 // even when the source root does not directly own the part labels (as is the case with
3917 // default KiCad 3D models produced by CadQuery). It is also immune to the
3918 // "not self-contained" restriction of TDocStd_XLinkTool::Copy, so Fusion 360 STEP files
3919 // with linked components work as well.
3920 TDF_Label d_targetLabel = d_assy->NewShape();
3921
3922 if( !XCAFDoc_Editor::Extract( frshapes, d_targetLabel, Standard_False ) )
3923 {
3924 m_reporter->Report( wxT( "Failed to transfer model." ), RPT_SEVERITY_ERROR );
3925 return TDF_Label();
3926 }
3927
3928 if( aScale.x != 1.0 || aScale.y != 1.0 || aScale.z != 1.0 )
3929 rescaleShapes( d_targetLabel, gp_XYZ( aScale.x, aScale.y, aScale.z ) );
3930
3931 return d_targetLabel;
3932}
3933
3934
3935bool STEP_PCB_MODEL::performMeshing( Handle( XCAFDoc_ShapeTool ) & aShapeTool )
3936{
3937 TDF_LabelSequence freeShapes;
3938 aShapeTool->GetFreeShapes( freeShapes );
3939
3940 m_reporter->Report( wxT( "Meshing model" ), RPT_SEVERITY_DEBUG );
3941
3942 // GLTF is a mesh format, we have to trigger opencascade to mesh the shapes we composited into the asesmbly
3943 // To mesh models, lets just grab the free shape root and execute on them
3944 for( Standard_Integer i = 1; i <= freeShapes.Length(); ++i )
3945 {
3946 TDF_Label label = freeShapes.Value( i );
3947 TopoDS_Shape shape;
3948 aShapeTool->GetShape( label, shape );
3949
3950 // These deflection values basically affect the accuracy of the mesh generated, a tighter
3951 // deflection will result in larger meshes
3952 // We could make this a tunable parameter, but for now fix it
3953 const Standard_Real linearDeflection = 0.14;
3954 const Standard_Real angularDeflection = DEG2RAD( 30.0 );
3955 BRepMesh_IncrementalMesh mesh( shape, linearDeflection, Standard_False, angularDeflection,
3956 Standard_True );
3957 }
3958
3959 return true;
3960}
3961
3962
3963bool STEP_PCB_MODEL::WriteGLTF( const wxString& aFileName )
3964{
3965 /*if( !isBoardOutlineValid() )
3966 {
3967 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
3968 aFileName ),
3969 RPT_SEVERITY_ERROR );
3970 return false;
3971 }*/
3972
3974
3975 performMeshing( m_assy );
3976
3977 wxFileName fn( aFileName );
3978
3979 const char* tmpGltfname = "$tempfile$.glb";
3980 RWGltf_CafWriter cafWriter( tmpGltfname, true );
3981
3982 cafWriter.SetTransformationFormat( RWGltf_WriterTrsfFormat_Compact );
3983 cafWriter.ChangeCoordinateSystemConverter().SetInputLengthUnit( 0.001 );
3984 cafWriter.ChangeCoordinateSystemConverter().SetInputCoordinateSystem(
3985 RWMesh_CoordinateSystem_Zup );
3986#if OCC_VERSION_HEX >= 0x070700
3987 cafWriter.SetParallel( true );
3988#endif
3989 TColStd_IndexedDataMapOfStringString metadata;
3990
3991 metadata.Add( TCollection_AsciiString( "pcb_name" ),
3992 TCollection_ExtendedString( fn.GetName().wc_str() ) );
3993 metadata.Add( TCollection_AsciiString( "source_pcb_file" ),
3994 TCollection_ExtendedString( fn.GetFullName().wc_str() ) );
3995 metadata.Add( TCollection_AsciiString( "generator" ),
3996 TCollection_AsciiString( wxString::Format( wxS( "KiCad %s" ), GetSemanticVersion() ).ToAscii() ) );
3997 metadata.Add( TCollection_AsciiString( "generated_at" ),
3998 TCollection_AsciiString( GetISO8601CurrentDateTime().ToAscii() ) );
3999
4000 bool success = true;
4001
4002 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
4003 wxString currCWD = wxGetCwd();
4004 wxString workCWD = fn.GetPath();
4005
4006 if( !workCWD.IsEmpty() )
4007 wxSetWorkingDirectory( workCWD );
4008
4009 success = cafWriter.Perform( m_doc, metadata, Message_ProgressRange() );
4010
4011 if( success )
4012 {
4013 // Preserve the permissions of the current file
4014 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpGltfname );
4015
4016 if( !wxRenameFile( tmpGltfname, fn.GetFullName(), true ) )
4017 {
4018 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'." ),
4019 tmpGltfname,
4020 fn.GetFullName() ),
4022 success = false;
4023 }
4024 }
4025
4026 wxSetWorkingDirectory( currCWD );
4027
4028 return success;
4029}
4030
4031
4032bool STEP_PCB_MODEL::WritePLY( const wxString& aFileName )
4033{
4034#if OCC_VERSION_HEX < 0x070700
4035 m_reporter->Report( wxT( "PLY export is not supported before OCCT 7.7.0" ), RPT_SEVERITY_ERROR );
4036 return false;
4037#else
4038
4039 if( !isBoardOutlineValid() )
4040 {
4041 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
4042 aFileName ),
4044 return false;
4045 }
4046
4048
4049 performMeshing( m_assy );
4050
4051 wxFileName fn( aFileName );
4052
4053 const char* tmpFname = "$tempfile$.ply";
4054 RWPly_CafWriter cafWriter( tmpFname );
4055
4056 cafWriter.SetFaceId( true ); // TODO: configurable SetPartId/SetFaceId
4057 cafWriter.ChangeCoordinateSystemConverter().SetInputLengthUnit( 0.001 );
4058 cafWriter.ChangeCoordinateSystemConverter().SetInputCoordinateSystem( RWMesh_CoordinateSystem_Zup );
4059
4060 TColStd_IndexedDataMapOfStringString metadata;
4061
4062 metadata.Add( TCollection_AsciiString( "pcb_name" ),
4063 TCollection_ExtendedString( fn.GetName().wc_str() ) );
4064 metadata.Add( TCollection_AsciiString( "source_pcb_file" ),
4065 TCollection_ExtendedString( fn.GetFullName().wc_str() ) );
4066 metadata.Add( TCollection_AsciiString( "generator" ),
4067 TCollection_AsciiString( wxString::Format( wxS( "KiCad %s" ),
4068 GetSemanticVersion() ).ToAscii() ) );
4069 metadata.Add( TCollection_AsciiString( "generated_at" ),
4070 TCollection_AsciiString( GetISO8601CurrentDateTime().ToAscii() ) );
4071
4072 bool success = true;
4073
4074 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
4075 wxString currCWD = wxGetCwd();
4076 wxString workCWD = fn.GetPath();
4077
4078 if( !workCWD.IsEmpty() )
4079 wxSetWorkingDirectory( workCWD );
4080
4081 success = cafWriter.Perform( m_doc, metadata, Message_ProgressRange() );
4082
4083 if( success )
4084 {
4085 // Preserve the permissions of the current file
4086 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpFname );
4087
4088 if( !wxRenameFile( tmpFname, fn.GetFullName(), true ) )
4089 {
4090 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'." ),
4091 tmpFname,
4092 fn.GetFullName() ),
4094 success = false;
4095 }
4096 }
4097
4098 wxSetWorkingDirectory( currCWD );
4099
4100 return success;
4101#endif
4102}
4103
4104
4105bool STEP_PCB_MODEL::WriteSTL( const wxString& aFileName )
4106{
4107 if( !isBoardOutlineValid() )
4108 {
4109 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
4110 aFileName ),
4112 return false;
4113 }
4114
4116
4117 performMeshing( m_assy );
4118
4119 wxFileName fn( aFileName );
4120
4121 const char* tmpFname = "$tempfile$.stl";
4122
4123 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
4124 wxString currCWD = wxGetCwd();
4125 wxString workCWD = fn.GetPath();
4126
4127 if( !workCWD.IsEmpty() )
4128 wxSetWorkingDirectory( workCWD );
4129
4130 bool success = StlAPI_Writer().Write( getOneShape( m_assy ), tmpFname );
4131
4132 if( success )
4133 {
4134 // Preserve the permissions of the current file
4135 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpFname );
4136
4137 if( !wxRenameFile( tmpFname, fn.GetFullName(), true ) )
4138 {
4139 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'." ),
4140 tmpFname,
4141 fn.GetFullName() ),
4143 success = false;
4144 }
4145 }
4146
4147 wxSetWorkingDirectory( currCWD );
4148
4149 return success;
4150}
4151
4152
4153
4154bool STEP_PCB_MODEL::WriteU3D( const wxString& aFileName )
4155{
4156 if( !isBoardOutlineValid() )
4157 {
4158 m_reporter->Report(
4159 wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'.\n" ), aFileName ),
4161 return false;
4162 }
4163
4165
4166 performMeshing( m_assy );
4167
4168 wxFileName fn( aFileName );
4169
4170 const char* tmpFname = "$tempfile$.u3d";
4171
4172 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
4173 wxString currCWD = wxGetCwd();
4174 wxString workCWD = fn.GetPath();
4175
4176 if( !workCWD.IsEmpty() )
4177 wxSetWorkingDirectory( workCWD );
4178
4179 U3D::WRITER writer( tmpFname );
4180 bool success = writer.Perform( m_doc );
4181 if( success )
4182 {
4183 // Preserve the permissions of the current file
4184 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpFname );
4185
4186 if( !wxRenameFile( tmpFname, fn.GetFullName(), true ) )
4187 {
4188 m_reporter->Report( wxString::Format( wxT( "Cannot rename temporary file '%s' to '%s'.\n" ), tmpFname,
4189 fn.GetFullName() ),
4191 success = false;
4192 }
4193 }
4194
4195 wxSetWorkingDirectory( currCWD );
4196
4197 return success;
4198}
4199
4200
4201bool STEP_PCB_MODEL::WritePDF( const wxString& aFileName )
4202{
4203 if( !isBoardOutlineValid() )
4204 {
4205 m_reporter->Report(
4206 wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'.\n" ), aFileName ),
4208 return false;
4209 }
4210
4212
4213 performMeshing( m_assy );
4214
4215 wxFileName fn( aFileName );
4216
4217 wxFileName u3dTmpfn = wxFileName::CreateTempFileName( "" );
4218 wxFileName pdfTmpfn = wxFileName::CreateTempFileName( "" );
4219
4220 U3D::WRITER writer( u3dTmpfn.GetFullPath().ToStdString() );
4221 bool success = writer.Perform( m_doc );
4222
4223 // PDF test
4224 std::unique_ptr<PDF_PLOTTER> plotter = std::make_unique<PDF_PLOTTER>();
4225
4226 plotter->SetColorMode( true );
4227 plotter->Set3DExport( true );
4228 plotter->SetCreator( wxT( "Mark's awesome 3d exporter" ) );
4229 KIGFX::PCB_RENDER_SETTINGS renderSettings;
4230 plotter->SetRenderSettings( &renderSettings );
4231
4232 if( !plotter->OpenFile( pdfTmpfn.GetFullPath() ) )
4233 {
4234 m_reporter->Report( wxString::Format( wxT( "Cannot open temporary file '%s'.\n" ), pdfTmpfn.GetFullPath() ),
4236 success = false;
4237 }
4238 else
4239 {
4240 plotter->StartPlot( "1", "3D Model" );
4241 double fov_degrees = 16.5f;
4242
4243 // kind of an arbitrary distance determination
4244 float distance = sqrt( writer.GetMeshBoundingBox().SquareExtent() ) * 3;
4245
4246 std::vector<PDF_3D_VIEW> views;
4247
4248 VECTOR3D camTarget = writer.GetCenter();
4249
4250
4251 std::vector<float> c2wMatrix =
4252 PDF_PLOTTER::CreateC2WMatrixFromAngles( camTarget, distance, 180.0f, -75.0f, 25.0f );
4253
4254 views.emplace_back( PDF_3D_VIEW{
4255 .m_name = "Default",
4256 .m_cameraMatrix = c2wMatrix,
4257 .m_cameraCenter = (float) distance,
4258 .m_fov = (float) fov_degrees,
4259 } );
4260
4261
4262
4263 c2wMatrix = PDF_PLOTTER::CreateC2WMatrixFromAngles( camTarget, distance, 180.0, 0.0f, 0.0f );
4264
4265 views.emplace_back( PDF_3D_VIEW{
4266 .m_name = "Top",
4267 .m_cameraMatrix = c2wMatrix,
4268 .m_cameraCenter = (float) distance,
4269 .m_fov = (float) fov_degrees,
4270 } );
4271
4272
4273
4274 c2wMatrix = PDF_PLOTTER::CreateC2WMatrixFromAngles( camTarget, distance, 0.0, 0.0f, 0.0f );
4275
4276 views.emplace_back( PDF_3D_VIEW{
4277 .m_name = "Bottom",
4278 .m_cameraMatrix = c2wMatrix,
4279 .m_cameraCenter = (float) distance,
4280 .m_fov = (float) fov_degrees,
4281 } );
4282
4283
4284
4285 c2wMatrix = PDF_PLOTTER::CreateC2WMatrixFromAngles( camTarget, distance, 90.0f, -90.0f, 90.0f );
4286
4287 views.emplace_back( PDF_3D_VIEW{
4288 .m_name = "Front",
4289 .m_cameraMatrix = c2wMatrix,
4290 .m_cameraCenter = (float) distance,
4291 .m_fov = (float) fov_degrees,
4292 } );
4293
4294 plotter->Plot3DModel( u3dTmpfn.GetFullPath(), views );
4295 plotter->EndPlot();
4296 }
4297
4298 if( success )
4299 {
4300 // Preserve the permissions of the current file
4301 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), pdfTmpfn.GetFullPath() );
4302
4303 if( !wxRenameFile( pdfTmpfn.GetFullPath(), fn.GetFullPath(), true ) )
4304 {
4305 m_reporter->Report( wxString::Format( wxT( "Cannot rename temporary file '%s' to '%s'.\n" ),
4306 pdfTmpfn.GetFullPath(), fn.GetFullPath() ),
4308 success = false;
4309 }
4310 }
4311
4312 wxRemoveFile( u3dTmpfn.GetFullPath() );
4313
4314 return success;
4315}
void ApplyExtrusionTransform(SHAPE_POLY_SET &aOutline, const EXTRUDED_3D_BODY *aBody, const VECTOR2I &aFpPos)
Apply 2D extrusion transforms (rotation, scale, offset) to an outline.
bool GetExtrusionPinOutline(const FOOTPRINT *aFootprint, SHAPE_POLY_SET &aPinPoly)
Get the pin outline polygons for extruded THT pin rendering.
int index
const char * name
@ ERROR_OUTSIDE
@ ERROR_INSIDE
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:125
bool IsPrmSpecified(const wxString &aPrmValue)
BOARD_STACKUP_ITEM_TYPE
@ BS_ITEM_TYPE_COPPER
@ BS_ITEM_TYPE_SILKSCREEN
@ BS_ITEM_TYPE_DIELECTRIC
@ BS_ITEM_TYPE_SOLDERMASK
BOX2< VECTOR2I > BOX2I
Definition box2.h:922
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:125
int GetX() const
Definition board_item.h:119
FOOTPRINT * GetParentFootprint() const
int GetMaxError() const
Manage one layer needed to make a physical board.
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.
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
const EXTRUDED_3D_BODY * GetExtrudedBody() const
Definition footprint.h:398
wxString GetReferenceAsString() const
Definition footprint.h:838
VECTOR2I GetPosition() const override
Definition footprint.h:405
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:105
double r
Red component.
Definition color4d.h:393
double g
Green component.
Definition color4d.h:394
COLOR4D & Darken(double aFactor)
Makes the color darker by a given factor.
Definition color4d.h:227
double a
Alpha component.
Definition color4d.h:396
double b
Blue component.
Definition color4d.h:395
PCB specific render settings.
Definition pcb_painter.h:82
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:313
Definition pad.h:55
PAD_PROP GetProperty() const
Definition pad.h:566
LSET GetLayerSet() const override
Return a std::bitset of all layers on which the item physically resides.
Definition pad.h:560
bool FlashLayer(int aLayer, bool aOnlyCheckIfPermitted=false) const
Check to see whether the pad should be flashed on the specific layer.
Definition pad.cpp:439
bool IsOnLayer(PCB_LAYER_ID aLayer) const override
Test to see if this object is on the given layer.
Definition pad.h:917
const VECTOR2I & GetDrillSize() const
Definition pad.h:317
PAD_ATTRIB GetAttribute() const
Definition pad.h:563
const wxString & GetNumber() const
Definition pad.h:137
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:2590
std::shared_ptr< SHAPE_SEGMENT > GetEffectiveHoleShape() const override
Return a SHAPE_SEGMENT object representing the pad's hole.
Definition pad.cpp:1066
static std::vector< float > CreateC2WMatrixFromAngles(const VECTOR3D &aTargetPosition, float aCameraDistance, float aYawDegrees, float aPitchDegrees, float aRollDegrees)
Generates the camera to world matrix for use with a 3D View.
A pure virtual class used to derive REPORTER objects from.
Definition reporter.h:75
virtual REPORTER & Report(const wxString &aText, SEVERITY aSeverity=RPT_SEVERITY_UNDEFINED)
Report a string with a given severity.
Definition reporter.h:104
Definition seg.h:42
VECTOR2I A
Definition seg.h:49
VECTOR2I B
Definition seg.h:50
const VECTOR2I & GetArcMid() const
Definition shape_arc.h:120
const VECTOR2I & GetP1() const
Definition shape_arc.h:119
double GetRadius() const
const VECTOR2I & GetP0() const
Definition shape_arc.h:118
const VECTOR2I & GetCenter() const
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 override
bool MakeShapeAsThickSegment(TopoDS_Shape &aShape, const VECTOR2D &aStartPoint, const VECTOR2D &aEndPoint, double aWidth, double aThickness, double aZposition, const VECTOR2D &aOrigin)
Make a segment shape based on start and end point.
OUTPUT_FORMAT m_outFmt
The current output format for created file.
void SetCopperColor(double r, double g, double b)
bool WritePLY(const wxString &aFileName)
bool AddCountersink(const VECTOR2I &aPosition, int aDiameter, int aDepth, int aAngle, bool aFrontSide, const VECTOR2D &aOrigin)
Add a countersink shape to remove board material from the top or bottom of a hole.
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)
std::vector< TopoDS_Shape > m_board_back_mask
wxString m_pcbName
Name of the PCB, which will most likely be the file name of the path.
std::vector< TopoDS_Shape > m_boardCutouts
bool AddPolygonShapes(const SHAPE_POLY_SET *aPolyShapes, PCB_LAYER_ID aLayer, const VECTOR2D &aOrigin, const wxString &aNetname)
bool CreatePCB(SHAPE_POLY_SET &aOutline, const VECTOR2D &aOrigin, bool aPushBoardBody)
void getBoardBodyZPlacement(double &aZPos, double &aThickness)
TDF_Label transferModel(Handle(TDocStd_Document)&source, Handle(TDocStd_Document) &dest, const VECTOR3D &aScale)
TDF_Label m_assy_label
bool getModelLocation(bool aBottom, const VECTOR2D &aPosition, double aRotation, const VECTOR3D &aOffset, const VECTOR3D &aOrientation, TopLoc_Location &aLocation)
void SetFuseShapes(bool aValue)
bool WriteXAO(const wxString &aFileName)
bool WriteGLTF(const wxString &aFileName)
Write the assembly in binary GLTF Format.
REPORTER * m_reporter
std::vector< TDF_Label > m_pcb_labels
STEP_PCB_MODEL(const wxString &aPcbName, REPORTER *aReporter)
std::vector< EXTRUDED_BODY_ENTRY > m_extruded_bodies
bool AddBackdrill(const SHAPE_SEGMENT &aShape, PCB_LAYER_ID aLayerStart, PCB_LAYER_ID aLayerEnd, const VECTOR2D &aOrigin)
Add a backdrill hole shape to remove board material and copper plating.
void getLayerZPlacement(PCB_LAYER_ID aLayer, double &aZPos, double &aThickness)
std::vector< TopoDS_Shape > m_board_outlines
void SetSimplifyShapes(bool aValue)
bool WriteU3D(const wxString &aFileName)
bool getModelLabel(const wxString &aBaseName, const wxString &aFileName, const std::vector< wxString > &aAltFilenames, VECTOR3D aScale, TDF_Label &aLabel, bool aSubstituteModels, wxString *aErrorMessage=nullptr)
Load a 3D model data.
bool readVRML(Handle(TDocStd_Document) &aDoc, const char *aFname)
void SetPadColor(double r, double g, double b)
bool AddCounterbore(const VECTOR2I &aPosition, int aDiameter, int aDepth, bool aFrontSide, const VECTOR2D &aOrigin)
Add a counterbore shape to remove board material from the top or bottom of a hole.
void SetEnabledLayers(const LSET &aLayers)
void SetExtraPadThickness(bool aValue)
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)
std::vector< TopoDS_Shape > m_board_front_silk
bool AddExtrudedPins(const FOOTPRINT *aFootprint, bool aBottom, double aStandoff, const VECTOR2D &aOrigin)
Add metallic pin extrusions for through-hole pads.
Handle(XCAFApp_Application) m_app
bool WritePDF(const wxString &aFileName)
virtual ~STEP_PCB_MODEL()
std::vector< TopoDS_Shape > m_board_front_mask
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
std::map< PCB_LAYER_ID, int > GetCopperLayerKnockouts(int aDiameter, int aDepth, int aAngle, bool aFrontSide)
Get the knockout diameters for copper layers that a counterbore or countersink crosses.
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 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)
std::vector< TopoDS_Shape > m_board_back_silk
void getCopperLayerZPlacement(PCB_LAYER_ID aLayer, double &aZPos, double &aThickness)
bool AddComponent(const wxString &aBaseName, const wxString &aFileName, const std::vector< wxString > &aAltFilenames, const wxString &aRefDes, bool aBottom, VECTOR2D aPosition, double aRotation, VECTOR3D aOffset, VECTOR3D aOrientation, VECTOR3D aScale, bool aSubstituteModels=true)
bool AddExtrudedBody(const SHAPE_POLY_SET &aOutline, bool aBottom, double aStandoff, double aHeight, const VECTOR2D &aOrigin, uint32_t aColor, EXTRUSION_MATERIAL aMaterial, const wxString &aRefDes)
Add an extruded 3D body from a 2D outline polygon.
std::map< wxString, std::vector< std::pair< gp_Pnt, TopoDS_Shape > > > m_pad_points
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)
bool Perform(const Handle(TDocStd_Document) &aDocument)
Definition writer.cpp:967
const VECTOR3D & GetCenter() const
Definition writer.h:209
const Bnd_Box & GetMeshBoundingBox() const
Definition writer.h:210
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
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 _(s)
static constexpr EDA_ANGLE ANGLE_360
Definition eda_angle.h:417
EXTRUSION_MATERIAL
Definition footprint.h:95
const wxChar *const traceKiCad2Step
Flag to enable KiCad2Step debug tracing.
Handle(KICAD3D_INFO) KICAD3D_INFO
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:782
bool IsBackLayer(PCB_LAYER_ID aLayerId)
Layer classification: check if it's a back layer.
Definition layer_ids.h:805
bool IsCopperLayer(int aLayerId)
Test whether a layer is a copper layer.
Definition layer_ids.h:679
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:55
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
@ PTH
Plated through hole pad.
Definition padstack.h:98
@ CASTELLATED
a pad with a castellated through hole
Definition padstack.h:121
#define _HKI(x)
Definition page_info.cpp:44
Plotting engines similar to ps (PostScript, Gerber, svg)
static float distance(const SFVEC2UI &a, const SFVEC2UI &b)
@ 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()
KIBIS top(path, &reporter)
std::vector< std::string > header
nlohmann::json output
VECTOR2I center
int radius
VECTOR2I end
SHAPE_CIRCLE circle(c.m_circle_center, c.m_circle_radius)
#define M_PI
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
static thread_pool * tp
BS::priority_thread_pool thread_pool
Definition thread_pool.h:31
wxLogTrace helper definitions.
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:687
VECTOR2< double > VECTOR2D
Definition vector2d.h:686
VECTOR3< double > VECTOR3D
Definition vector3.h:230