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