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
1698void STEP_PCB_MODEL::getBoardBodyZPlacement( double& aZPos, double& aThickness )
1699{
1700 double f_pos, f_thickness;
1701 double b_pos, b_thickness;
1702 getLayerZPlacement( F_Cu, f_pos, f_thickness );
1703 getLayerZPlacement( B_Cu, b_pos, b_thickness );
1704 double top = std::min( f_pos, f_pos + f_thickness );
1705 double bottom = std::max( b_pos, b_pos + b_thickness );
1706
1707 aThickness = ( top - bottom );
1708 aZPos = bottom;
1709
1710 wxASSERT( aZPos == 0.0 );
1711}
1712
1713
1714bool STEP_PCB_MODEL::AddExtrudedBody( const SHAPE_POLY_SET& aOutline, bool aBottom, double aStandoff, double aHeight,
1715 const VECTOR2D& aOrigin, uint32_t aColor, EXTRUSION_MATERIAL aMaterial,
1716 const wxString& aRefDes )
1717{
1718 double f_pos, f_thickness;
1719 double b_pos, b_thickness;
1720 getLayerZPlacement( F_Cu, f_pos, f_thickness );
1721 getLayerZPlacement( B_Cu, b_pos, b_thickness );
1722
1723 double boardSurfaceZ;
1724
1725 if( !aBottom )
1726 boardSurfaceZ = std::max( f_pos, f_pos + f_thickness );
1727 else
1728 boardSurfaceZ = std::min( b_pos, b_pos + b_thickness );
1729
1730 double bodyThickness = aHeight - aStandoff;
1731 double zBot;
1732
1733 if( !aBottom )
1734 zBot = boardSurfaceZ + aStandoff;
1735 else
1736 zBot = boardSurfaceZ - aHeight;
1737
1738 m_extruded_bodies.push_back( { {}, {}, aRefDes, aColor, aMaterial } );
1739 return MakeShapes( m_extruded_bodies.back().bodyShapes, aOutline, m_simplifyShapes, bodyThickness, zBot, aOrigin );
1740}
1741
1742
1743bool STEP_PCB_MODEL::AddExtrudedPins( const FOOTPRINT* aFootprint, bool aBottom, double aStandoff,
1744 const VECTOR2D& aOrigin )
1745{
1746 if( aStandoff <= 0.0 )
1747 return false;
1748
1749 SHAPE_POLY_SET pinPoly;
1750
1751 if( !GetExtrusionPinOutline( aFootprint, pinPoly ) )
1752 return false;
1753
1754 const EXTRUDED_3D_BODY* body = aFootprint->GetExtrudedBody();
1755
1756 if( body )
1757 {
1758 VECTOR2I fpPos = aFootprint->GetPosition();
1759 ApplyExtrusionTransform( pinPoly, body, fpPos );
1760 }
1761
1762 double f_pos, f_thickness;
1763 double b_pos, b_thickness;
1764 getLayerZPlacement( F_Cu, f_pos, f_thickness );
1765 getLayerZPlacement( B_Cu, b_pos, b_thickness );
1766
1767 double boardTopZ = std::max( f_pos, f_pos + f_thickness );
1768 double boardBotZ = std::min( b_pos, b_pos + b_thickness );
1769
1770 static const double c_protrusion = 1.0; // 1mm below opposite side
1771
1772 double pinZBot, pinHeight;
1773
1774 if( !aBottom )
1775 {
1776 pinZBot = boardBotZ - c_protrusion;
1777 pinHeight = ( boardTopZ + aStandoff ) - pinZBot;
1778 }
1779 else
1780 {
1781 double pinZTop = boardTopZ + c_protrusion;
1782 pinZBot = boardBotZ - aStandoff;
1783 pinHeight = pinZTop - pinZBot;
1784 }
1785
1786 if( m_extruded_bodies.empty() )
1787 return false;
1788
1789 return MakeShapes( m_extruded_bodies.back().pinShapes, pinPoly, m_simplifyShapes, pinHeight, pinZBot, aOrigin );
1790}
1791
1792
1794 const VECTOR2D& aOrigin, const wxString& aNetname )
1795{
1796 bool success = true;
1797
1798 if( aPolyShapes->IsEmpty() )
1799 return true;
1800
1801 if( !m_enabledLayers.Contains( aLayer ) )
1802 return true;
1803
1804 double z_pos, thickness;
1805 getLayerZPlacement( aLayer, z_pos, thickness );
1806
1807 std::vector<TopoDS_Shape>* targetVec = nullptr;
1808
1809 if( IsCopperLayer( aLayer ) )
1810 targetVec = &m_board_copper[aNetname];
1811 else if( aLayer == F_SilkS )
1812 targetVec = &m_board_front_silk;
1813 else if( aLayer == B_SilkS )
1814 targetVec = &m_board_back_silk;
1815 else if( aLayer == F_Mask )
1816 targetVec = &m_board_front_mask;
1817 else
1818 targetVec = &m_board_back_mask;
1819
1820 if( !MakeShapes( *targetVec, *aPolyShapes, m_simplifyShapes, thickness, z_pos, aOrigin ) )
1821 {
1822 m_reporter->Report( wxString::Format( _( "Could not add shape (%d points) to copper layer %s." ),
1823 aPolyShapes->FullPointCount(),
1824 LayerName( aLayer ) ),
1826
1827 success = false;
1828 }
1829
1830 return success;
1831}
1832
1833
1834bool STEP_PCB_MODEL::AddComponent( const wxString& aBaseName, const wxString& aFileName,
1835 const std::vector<wxString>& aAltFilenames,
1836 const wxString& aRefDes, bool aBottom, VECTOR2D aPosition,
1837 double aRotation, VECTOR3D aOffset, VECTOR3D aOrientation,
1838 VECTOR3D aScale, bool aSubstituteModels )
1839{
1840 if( aFileName.empty() )
1841 {
1842 m_reporter->Report( wxString::Format( _( "No model defined for %s." ), aRefDes ),
1844 return false;
1845 }
1846
1847 m_reporter->Report( wxString::Format( wxT( "Adding component %s." ), aRefDes ), RPT_SEVERITY_DEBUG );
1848
1849 // first retrieve a label
1850 TDF_Label lmodel;
1851 wxString errorMessage;
1852
1853 if( !getModelLabel( aBaseName, aFileName, aAltFilenames, aScale, lmodel, aSubstituteModels,
1854 &errorMessage ) )
1855 {
1856 if( errorMessage.IsEmpty() )
1857 errorMessage.Printf( _( "No model for filename '%s'." ), aFileName );
1858
1859 m_reporter->Report( errorMessage, RPT_SEVERITY_ERROR );
1860 return false;
1861 }
1862
1863 // calculate the Location transform
1864 TopLoc_Location toploc;
1865
1866 if( !getModelLocation( aBottom, aPosition, aRotation, aOffset, aOrientation, toploc ) )
1867 {
1868 m_reporter->Report(
1869 wxString::Format( _( "No location data for filename '%s'." ), aFileName ),
1871 return false;
1872 }
1873
1874 // add the located sub-assembly
1875 TDF_Label llabel = m_assy->AddComponent( m_assy_label, lmodel, toploc );
1876
1877 if( llabel.IsNull() )
1878 {
1879 m_reporter->Report(
1880 wxString::Format( _( "Could not add component with filename '%s'." ), aFileName ),
1882 return false;
1883 }
1884
1885 m_pcb_labels.push_back( llabel );
1886
1887 // attach the RefDes name
1888 TCollection_ExtendedString refdes( aRefDes.utf8_str() );
1889 TDataStd_Name::Set( llabel, refdes );
1890
1891 KICAD3D_INFO::Set( llabel, KICAD3D_MODEL_TYPE::COMPONENT, aRefDes.utf8_string() );
1892
1893 // Rebuild compound shapes for all assemblies so that the newly added component
1894 // contributes to its parent's aggregate TopoDS_Shape. This is required when the
1895 // transferred sub-model was synthesized from a source label tree (via
1896 // XCAFDoc_Editor::Extract), because Extract leaves the assembly shape as an empty
1897 // compound until UpdateAssemblies is invoked. Without this the downstream writers
1898 // (STEPCAFControl_Writer, RWGltf_CafWriter, RWObj_CafWriter) see no geometry for
1899 // the added component.
1900 m_assy->UpdateAssemblies();
1901
1902 return true;
1903}
1904
1905
1907{
1908 m_enabledLayers = aLayers;
1909}
1910
1911
1913{
1914 m_fuseShapes = aValue;
1915}
1916
1917
1919{
1920 m_simplifyShapes = aValue;
1921}
1922
1923
1925{
1926 m_stackup = aStackup;
1927}
1928
1929
1930void STEP_PCB_MODEL::SetNetFilter( const wxString& aFilter )
1931{
1932 m_netFilter = aFilter;
1933}
1934
1935
1937{
1938 m_extraPadThickness = aValue;
1939}
1940
1941
1942void STEP_PCB_MODEL::SetCopperColor( double r, double g, double b )
1943{
1944 m_copperColor[0] = r;
1945 m_copperColor[1] = g;
1946 m_copperColor[2] = b;
1947}
1948
1949
1950void STEP_PCB_MODEL::SetPadColor( double r, double g, double b )
1951{
1952 m_padColor[0] = r;
1953 m_padColor[1] = g;
1954 m_padColor[2] = b;
1955}
1956
1957
1959{
1960 // Ensure a minimal value (in mm)
1961 m_mergeOCCMaxDist = aDistance;
1962}
1963
1964
1966{
1967 return m_pcb_labels.size() > 0;
1968}
1969
1970
1971bool STEP_PCB_MODEL::MakeShapeAsThickSegment( TopoDS_Shape& aShape, const VECTOR2D& aStartPoint,
1972 const VECTOR2D& aEndPoint, double aWidth, double aThickness,
1973 double aZposition, const VECTOR2D& aOrigin )
1974{
1975 // make a wide segment from 2 lines and 2 180 deg arcs
1976 // We need 6 points (3 per arcs)
1977 VECTOR2D coords[6];
1978
1979 // We build a horizontal segment, and after rotate it
1980 double len = ( aEndPoint - aStartPoint ).EuclideanNorm();
1981 double h_width = aWidth/2.0;
1982 // First is end point of first arc, and also start point of first line
1983 coords[0] = VECTOR2D{ 0.0, h_width };
1984
1985 // end point of first line and start point of second arc
1986 coords[1] = VECTOR2D{ len, h_width };
1987
1988 // middle point of second arc
1989 coords[2] = VECTOR2D{ len + h_width, 0.0 };
1990
1991 // start point of second line and end point of second arc
1992 coords[3] = VECTOR2D{ len, -h_width };
1993
1994 // end point of second line and start point of first arc
1995 coords[4] = VECTOR2D{ 0, -h_width };
1996
1997 // middle point of first arc
1998 coords[5] = VECTOR2D{ -h_width, 0.0 };
1999
2000 // Rotate and move to segment position
2001 EDA_ANGLE seg_angle( aEndPoint - aStartPoint );
2002
2003 for( int ii = 0; ii < 6; ii++ )
2004 {
2005 RotatePoint( coords[ii], VECTOR2D{ 0, 0 }, -seg_angle ),
2006 coords[ii] += aStartPoint;
2007 }
2008
2009
2010 // Convert to 3D points
2011 gp_Pnt coords3D[ 6 ];
2012
2013 for( int ii = 0; ii < 6; ii++ )
2014 {
2015 coords3D[ii] = gp_Pnt( pcbIUScale.IUTomm( coords[ii].x - aOrigin.x ),
2016 -pcbIUScale.IUTomm( coords[ii].y - aOrigin.y ), aZposition );
2017 }
2018
2019 // Build OpenCascade shape outlines
2020 BRepBuilderAPI_MakeWire wire;
2021 bool success = true;
2022
2023 // Short segments (distance between end points < m_mergeOCCMaxDist(in mm)) must be
2024 // skipped because OCC merge end points, and a null shape is created
2025 bool short_seg = pcbIUScale.IUTomm( len ) <= m_mergeOCCMaxDist;
2026
2027 try
2028 {
2029 TopoDS_Edge edge;
2030
2031 if( short_seg )
2032 {
2033 Handle( Geom_Circle ) circle = GC_MakeCircle( coords3D[1], // arc1 start point
2034 coords3D[2], // arc1 mid point
2035 coords3D[5] // arc2 mid point
2036 );
2037
2038 edge = BRepBuilderAPI_MakeEdge( circle );
2039 wire.Add( edge );
2040 }
2041 else
2042 {
2043 edge = BRepBuilderAPI_MakeEdge( coords3D[0], coords3D[1] );
2044 wire.Add( edge );
2045
2046 Handle( Geom_TrimmedCurve ) arcOfCircle =
2047 GC_MakeArcOfCircle( coords3D[1], // start point
2048 coords3D[2], // mid point
2049 coords3D[3] // end point
2050 );
2051 edge = BRepBuilderAPI_MakeEdge( arcOfCircle );
2052 wire.Add( edge );
2053
2054 edge = BRepBuilderAPI_MakeEdge( coords3D[3], coords3D[4] );
2055 wire.Add( edge );
2056
2057 Handle( Geom_TrimmedCurve ) arcOfCircle2 =
2058 GC_MakeArcOfCircle( coords3D[4], // start point
2059 coords3D[5], // mid point
2060 coords3D[0] // end point
2061 );
2062 edge = BRepBuilderAPI_MakeEdge( arcOfCircle2 );
2063 wire.Add( edge );
2064 }
2065 }
2066 catch( const Standard_Failure& e )
2067 {
2068 m_reporter->Report( wxString::Format( _( "OCC exception building shape segment: %s" ),
2069 e.GetMessageString() ),
2071 return false;
2072 }
2073
2074 BRepBuilderAPI_MakeFace face;
2075
2076 try
2077 {
2078 gp_Pln plane( coords3D[0], gp::DZ() );
2079 face = BRepBuilderAPI_MakeFace( plane, wire );
2080 }
2081 catch( const Standard_Failure& e )
2082 {
2083 m_reporter->Report( wxString::Format( _( "OCC exception building face: %s" ),
2084 e.GetMessageString() ),
2086 return false;
2087 }
2088
2089 if( aThickness != 0.0 )
2090 {
2091 aShape = BRepPrimAPI_MakePrism( face, gp_Vec( 0, 0, aThickness ) );
2092
2093 if( aShape.IsNull() )
2094 {
2095 m_reporter->Report( _( "Failed to create a prismatic shape" ),
2097 return false;
2098 }
2099 }
2100 else
2101 {
2102 aShape = face;
2103 }
2104
2105 return success;
2106}
2107
2108
2109bool STEP_PCB_MODEL::MakePolygonAsWall( TopoDS_Shape& aShape,
2110 SHAPE_POLY_SET& aPolySet,
2111 double aHeight,
2112 double aZposition, const VECTOR2D& aOrigin )
2113{
2114 std::vector<TopoDS_Shape> testShapes;
2115
2116 bool success = MakeShapes( testShapes, aPolySet, m_simplifyShapes,
2117 aHeight, aZposition, aOrigin );
2118
2119 if( testShapes.size() > 0 )
2120 aShape = testShapes.front();
2121 else
2122 success = false;
2123
2124 return success;
2125}
2126
2127
2128static wxString formatBBox( const BOX2I& aBBox )
2129{
2130 wxString str;
2131 UNITS_PROVIDER unitsProvider( pcbIUScale, EDA_UNITS::MM );
2132
2133 str << "x0: " << unitsProvider.StringFromValue( aBBox.GetLeft(), false ) << "; ";
2134 str << "y0: " << unitsProvider.StringFromValue( aBBox.GetTop(), false ) << "; ";
2135 str << "x1: " << unitsProvider.StringFromValue( aBBox.GetRight(), false ) << "; ";
2136 str << "y1: " << unitsProvider.StringFromValue( aBBox.GetBottom(), false );
2137
2138 return str;
2139}
2140
2141
2142static bool makeWireFromChain( BRepLib_MakeWire& aMkWire, const SHAPE_LINE_CHAIN& aChain,
2143 double aMergeOCCMaxDist, double aZposition, const VECTOR2D& aOrigin,
2144 REPORTER* aReporter )
2145{
2146 auto toPoint =
2147 [&]( const VECTOR2D& aKiCoords ) -> gp_Pnt
2148 {
2149 return gp_Pnt( pcbIUScale.IUTomm( aKiCoords.x - aOrigin.x ),
2150 -pcbIUScale.IUTomm( aKiCoords.y - aOrigin.y ), aZposition );
2151 };
2152
2153 try
2154 {
2155 auto addSegment = [&]( const VECTOR2I& aPt0, const VECTOR2I& aPt1 ) -> bool
2156 {
2157 if( aPt0 == aPt1 )
2158 return false;
2159
2160 gp_Pnt start = toPoint( aPt0 );
2161 gp_Pnt end = toPoint( aPt1 );
2162
2163 BRepBuilderAPI_MakeEdge mkEdge( start, end );
2164
2165 if( !mkEdge.IsDone() || mkEdge.Edge().IsNull() )
2166 {
2167 aReporter->Report( wxString::Format( _( "Failed to make segment edge (%d %d) -> (%d %d), "
2168 "skipping" ),
2169 aPt0.x, aPt0.y,
2170 aPt1.x, aPt1.y ),
2172 }
2173 else
2174 {
2175 aMkWire.Add( mkEdge.Edge() );
2176
2177 if( aMkWire.Error() != BRepLib_WireDone )
2178 {
2179 aReporter->Report( wxString::Format( _( "Failed to add segment edge (%d %d) -> (%d %d)" ),
2180 aPt0.x, aPt0.y,
2181 aPt1.x, aPt1.y ),
2183 return false;
2184 }
2185 }
2186
2187 return true;
2188 };
2189
2190 auto addArc = [&]( const VECTOR2I& aPt0, const SHAPE_ARC& aArc ) -> bool
2191 {
2192 // Do not export too short segments: they create broken shape because OCC thinks
2193 Handle( Geom_Curve ) curve;
2194
2195 if( aArc.GetCentralAngle() == ANGLE_360 )
2196 {
2197 gp_Ax2 axis = gp::XOY();
2198 axis.SetLocation( toPoint( aArc.GetCenter() ) );
2199
2200 curve = GC_MakeCircle( axis, pcbIUScale.IUTomm( aArc.GetRadius() ) ).Value();
2201 }
2202 else
2203 {
2204 curve = GC_MakeArcOfCircle( toPoint( aPt0 ), toPoint( aArc.GetArcMid() ),
2205 toPoint( aArc.GetP1() ) ).Value();
2206 }
2207
2208 if( curve.IsNull() )
2209 return false;
2210
2211 aMkWire.Add( BRepBuilderAPI_MakeEdge( curve ) );
2212
2213 if( !aMkWire.IsDone() )
2214 {
2215 aReporter->Report( wxString::Format( _( "Failed to add arc curve from (%d %d), arc p0 "
2216 "(%d %d), mid (%d %d), p1 (%d %d)" ),
2217 aPt0.x, aPt0.y,
2218 aArc.GetP0().x, aArc.GetP0().y,
2219 aArc.GetArcMid().x, aArc.GetArcMid().y,
2220 aArc.GetP1().x, aArc.GetP1().y ),
2222 return false;
2223 }
2224
2225 return true;
2226 };
2227
2228 VECTOR2I firstPt;
2229 VECTOR2I lastPt;
2230 bool isFirstShape = true;
2231
2232 for( int i = 0; i <= aChain.PointCount() && i != -1; i = aChain.NextShape( i ) )
2233 {
2234 if( i == 0 )
2235 {
2236 if( aChain.IsArcSegment( 0 ) && aChain.IsArcSegment( aChain.PointCount() - 1 )
2237 && aChain.ArcIndex( 0 ) == aChain.ArcIndex( aChain.PointCount() - 1 ) )
2238 {
2239 // Skip first arc (we should encounter it later)
2240 int nextShape = aChain.NextShape( i );
2241
2242 // If nextShape points to the end, then we have a circle.
2243 if( nextShape != -1 )
2244 i = nextShape;
2245 }
2246 }
2247
2248 if( isFirstShape )
2249 lastPt = aChain.CPoint( i );
2250
2251 bool isArc = aChain.IsArcSegment( i );
2252
2253 if( aChain.IsArcStart( i ) )
2254 {
2255 const SHAPE_ARC& currentArc = aChain.Arc( aChain.ArcIndex( i ) );
2256
2257 if( isFirstShape )
2258 {
2259 firstPt = currentArc.GetP0();
2260 lastPt = firstPt;
2261 }
2262
2263 if( addSegment( lastPt, currentArc.GetP0() ) )
2264 lastPt = currentArc.GetP0();
2265
2266 if( addArc( lastPt, currentArc ) )
2267 lastPt = currentArc.GetP1();
2268 }
2269 else if( !isArc )
2270 {
2271 const SEG& seg = aChain.CSegment( i );
2272
2273 if( isFirstShape )
2274 {
2275 firstPt = seg.A;
2276 lastPt = firstPt;
2277 }
2278
2279 if( addSegment( lastPt, seg.A ) )
2280 lastPt = seg.A;
2281
2282 if( addSegment( lastPt, seg.B ) )
2283 lastPt = seg.B;
2284 }
2285
2286 isFirstShape = false;
2287 }
2288
2289 if( lastPt != firstPt && !addSegment( lastPt, firstPt ) )
2290 {
2291 aReporter->Report( wxString::Format( _( "Failed to close wire at %d, %d -> %d, %d **" ),
2292 lastPt.x, lastPt.y,
2293 firstPt.x, firstPt.y ),
2295
2296 return false;
2297 }
2298 }
2299 catch( const Standard_Failure& e )
2300 {
2301 aReporter->Report( wxString::Format( _( "OCC exception creating wire: %s" ),
2302 e.GetMessageString() ),
2304 return false;
2305 }
2306
2307 return true;
2308}
2309
2310
2311bool STEP_PCB_MODEL::MakeShapes( std::vector<TopoDS_Shape>& aShapes, const SHAPE_POLY_SET& aPolySet,
2312 bool aConvertToArcs, double aThickness, double aZposition,
2313 const VECTOR2D& aOrigin )
2314{
2315 SHAPE_POLY_SET workingPoly = aPolySet;
2316 workingPoly.Simplify();
2317
2318 SHAPE_POLY_SET fallbackPoly = workingPoly;
2319
2320 if( aConvertToArcs )
2321 {
2322 SHAPE_POLY_SET approximated = workingPoly;
2323
2324 for( size_t polyId = 0; polyId < approximated.CPolygons().size(); polyId++ )
2325 {
2326 SHAPE_POLY_SET::POLYGON& polygon = approximated.Polygon( polyId );
2327
2328 for( size_t contId = 0; contId < polygon.size(); contId++ )
2329 polygon[contId] = approximateLineChainWithArcs( polygon[contId] );
2330 }
2331
2332 fallbackPoly = workingPoly;
2333 workingPoly = approximated;
2334
2335 // TODO: this is not accurate because it doesn't check arcs.
2336 /*if( approximated.IsSelfIntersecting() )
2337 {
2338 m_reporter->Report( wxString::Format( _( "Approximated polygon self-intersection check failed\n"
2339 "z: %g; bounding box: %s" ) ),
2340 aZposition,
2341 formatBBox( workingPoly.BBox() ) ),
2342 RPT_SEVERITY_ERROR );
2343 }
2344 else
2345 {
2346 fallbackPoly = workingPoly;
2347 workingPoly = approximated;
2348 }*/
2349 }
2350
2351#if 0 // No longer in use
2352 auto toPoint = [&]( const VECTOR2D& aKiCoords ) -> gp_Pnt
2353 {
2354 return gp_Pnt( pcbIUScale.IUTomm( aKiCoords.x - aOrigin.x ),
2355 -pcbIUScale.IUTomm( aKiCoords.y - aOrigin.y ), aZposition );
2356 };
2357#endif
2358
2359 gp_Pln basePlane( gp_Pnt( 0.0, 0.0, aZposition ),
2360 std::signbit( aThickness ) ? -gp::DZ() : gp::DZ() );
2361
2362 for( size_t polyId = 0; polyId < workingPoly.CPolygons().size(); polyId++ )
2363 {
2364 SHAPE_POLY_SET::POLYGON& polygon = workingPoly.Polygon( polyId );
2365
2366 auto tryMakeWire = [this, &aZposition,
2367 &aOrigin]( const SHAPE_LINE_CHAIN& aContour, bool aAllowRetry ) -> TopoDS_Wire
2368 {
2369 TopoDS_Wire wire;
2370 BRepLib_MakeWire mkWire;
2371
2372 makeWireFromChain( mkWire, aContour, m_mergeOCCMaxDist, aZposition, aOrigin, m_reporter );
2373
2374 if( mkWire.IsDone() )
2375 {
2376 wire = mkWire.Wire();
2377 }
2378 else
2379 {
2380 m_reporter->Report(
2381 wxString::Format( _( "Wire not done (contour points %d): OCC error %d\n"
2382 "z: %g; bounding box: %s" ),
2383 static_cast<int>( aContour.PointCount() ),
2384 static_cast<int>( mkWire.Error() ),
2385 formatBBox( aContour.BBox() ) ),
2387 }
2388
2389 if( !wire.IsNull() )
2390 {
2391 BRepAlgoAPI_Check check( wire, false, true );
2392
2393 if( !check.IsValid() )
2394 {
2395 m_reporter->Report( wxString::Format( _( "Wire self-interference check failed\n"
2396 "z: %g; bounding box: %s" ),
2397 aZposition,
2398 formatBBox( aContour.BBox() ) ),
2400
2401 wire.Nullify();
2402 }
2403 }
2404
2405 return wire;
2406 };
2407
2408 BRepBuilderAPI_MakeFace mkFace;
2409
2410 for( size_t contId = 0; contId < polygon.size(); contId++ )
2411 {
2412 try
2413 {
2414 // We allow retry when trying to convert polygon[contId] when a convert error
2415 // happens, using an equivalent polygon shape.
2416 bool allow_retry = aConvertToArcs ? true : false;
2417
2418 TopoDS_Wire wire = tryMakeWire( polygon[contId], allow_retry );
2419
2420 if( aConvertToArcs && wire.IsNull() )
2421 {
2422 m_reporter->Report( wxString::Format( _( "Using non-simplified polygon." ) ),
2424
2425 // Fall back to original shape. Do not allow retry
2426 allow_retry = false;
2427 wire = tryMakeWire( fallbackPoly.CPolygon( polyId )[contId], allow_retry );
2428 }
2429
2430 if( contId == 0 ) // Outline
2431 {
2432 if( !wire.IsNull() )
2433 {
2434 if( basePlane.Axis().Direction().Z() < 0 )
2435 wire.Reverse();
2436
2437 mkFace = BRepBuilderAPI_MakeFace( basePlane, wire );
2438 }
2439 else
2440 {
2441 m_reporter->Report( wxString::Format( wxT( "** Outline skipped **\n"
2442 "z: %g; bounding box: %s" ),
2443 aZposition,
2444 formatBBox( polygon[contId].BBox() ) ),
2446 break;
2447 }
2448 }
2449 else // Hole
2450 {
2451 if( !wire.IsNull() )
2452 {
2453 if( basePlane.Axis().Direction().Z() > 0 )
2454 wire.Reverse();
2455
2456 mkFace.Add( wire );
2457 }
2458 else
2459 {
2460 m_reporter->Report( wxString::Format( wxT( "** Hole skipped **\n"
2461 "z: %g; bounding box: %s" ),
2462 aZposition,
2463 formatBBox( polygon[contId].BBox() ) ),
2465 }
2466 }
2467 }
2468 catch( const Standard_Failure& e )
2469 {
2470 m_reporter->Report( wxString::Format( _( "OCC exception creating contour %d: %s" ),
2471 static_cast<int>( contId ),
2472 e.GetMessageString() ),
2474 return false;
2475 }
2476 }
2477
2478 if( mkFace.IsDone() )
2479 {
2480 TopoDS_Shape faceShape = mkFace.Shape();
2481
2482 if( aThickness != 0.0 )
2483 {
2484 TopoDS_Shape prism = BRepPrimAPI_MakePrism( faceShape, gp_Vec( 0, 0, aThickness ) );
2485 aShapes.push_back( prism );
2486
2487 if( prism.IsNull() )
2488 {
2489 m_reporter->Report( _( "Failed to create a prismatic shape" ), RPT_SEVERITY_ERROR );
2490 return false;
2491 }
2492 }
2493 else
2494 {
2495 aShapes.push_back( faceShape );
2496 }
2497 }
2498 else
2499 {
2500 m_reporter->Report( _( "** Face skipped **" ), RPT_SEVERITY_DEBUG );
2501 }
2502 }
2503
2504 return true;
2505}
2506
2507
2508// These colors are based on 3D viewer's colors and are different to "gbrjobColors"
2509static std::vector<FAB_LAYER_COLOR> s_soldermaskColors = {
2510 { NotSpecifiedPrm(), wxColor( 20, 51, 36 ) }, // Not specified, not in .gbrjob file
2511 { _HKI( "Green" ), wxColor( 20, 51, 36 ) }, // used in .gbrjob file
2512 { _HKI( "Red" ), wxColor( 181, 19, 21 ) }, // used in .gbrjob file
2513 { _HKI( "Blue" ), wxColor( 2, 59, 162 ) }, // used in .gbrjob file
2514 { _HKI( "Purple" ), wxColor( 32, 2, 53 ) }, // used in .gbrjob file
2515 { _HKI( "Black" ), wxColor( 11, 11, 11 ) }, // used in .gbrjob file
2516 { _HKI( "White" ), wxColor( 245, 245, 245 ) }, // used in .gbrjob file
2517 { _HKI( "Yellow" ), wxColor( 194, 195, 0 ) }, // used in .gbrjob file
2518 { _HKI( "User defined" ), wxColor( 128, 128, 128 ) } // Free; the name is a dummy name here
2519};
2520
2521
2522static bool colorFromStackup( BOARD_STACKUP_ITEM_TYPE aType, const wxString& aColorStr,
2523 COLOR4D& aColorOut )
2524{
2525 if( !IsPrmSpecified( aColorStr ) )
2526 return false;
2527
2528 if( aColorStr.StartsWith( wxT( "#" ) ) ) // User defined color
2529 {
2530 aColorOut = COLOR4D( aColorStr );
2531 return true;
2532 }
2533 else
2534 {
2535 const std::vector<FAB_LAYER_COLOR>& colors =
2536 ( aType == BS_ITEM_TYPE_SOLDERMASK || aType == BS_ITEM_TYPE_SILKSCREEN )
2538 : GetStandardColors( aType );
2539
2540 for( const FAB_LAYER_COLOR& fabColor : colors )
2541 {
2542 if( fabColor.GetName() == aColorStr )
2543 {
2544 aColorOut = fabColor.GetColor( aType );
2545 return true;
2546 }
2547 }
2548 }
2549
2550 return false;
2551}
2552
2553
2554bool STEP_PCB_MODEL::CreatePCB( SHAPE_POLY_SET& aOutline, const VECTOR2D& aOrigin, bool aPushBoardBody )
2555{
2556 if( m_hasPCB )
2557 {
2558 if( !isBoardOutlineValid() )
2559 return false;
2560
2561 return true;
2562 }
2563
2565
2566 Handle( XCAFDoc_VisMaterialTool ) visMatTool = XCAFDoc_DocumentTool::VisMaterialTool( m_doc->Main() );
2567
2568 m_hasPCB = true; // whether or not operations fail we note that CreatePCB has been invoked
2569
2570 // Support for more than one main outline (more than one board)
2571 m_reporter->Report( wxString::Format( wxT( "Build board outlines (%d outlines) with %d points." ),
2572 aOutline.OutlineCount(),
2573 aOutline.FullPointCount() ),
2575
2576 double boardThickness;
2577 double boardZPos;
2578 getBoardBodyZPlacement( boardZPos, boardThickness );
2579
2580#if 1
2581 // This code should work, and it is working most of time
2582 // However there are issues if the main outline is a circle with holes:
2583 // holes from vias and pads are not working
2584 // see bug https://gitlab.com/kicad/code/kicad/-/issues/17446
2585 // (Holes are missing from STEP export with circular PCB outline)
2586 // Hard to say if the bug is in our code or in OCC 7.7
2587 if( !MakeShapes( m_board_outlines, aOutline, false, boardThickness, boardZPos, aOrigin ) )
2588 {
2589 // Error
2590 m_reporter->Report( _( "OCC error creating main outline." ), RPT_SEVERITY_ERROR );
2591 }
2592#else
2593 // Workaround for bug #17446 Holes are missing from STEP export with circular PCB outline
2594 for( const SHAPE_POLY_SET::POLYGON& polygon : aOutline.CPolygons() )
2595 {
2596 for( size_t contId = 0; contId < polygon.size(); contId++ )
2597 {
2598 const SHAPE_LINE_CHAIN& contour = polygon[contId];
2599 SHAPE_POLY_SET polyset;
2600 polyset.Append( contour );
2601
2602 if( contId == 0 ) // main Outline
2603 {
2604 if( !MakeShapes( m_board_outlines, polyset, false, boardThickness, boardZPos,
2605 aOrigin ) )
2606 {
2607 m_reporter->Report( _( "OCC error creating main outline." ),
2609 }
2610 }
2611 else // Hole inside the main outline
2612 {
2613 if( !MakeShapes( m_boardCutouts, polyset, false, boardThickness, boardZPos,
2614 aOrigin ) )
2615 {
2616 m_reporter->Report( _( "OCC error creating hole in main outline." ),
2618 }
2619 }
2620 }
2621 }
2622#endif
2623
2624 // Even if we've disabled board body export, we still need the shapes for bounding box calculations.
2625 Bnd_Box brdBndBox;
2626
2627 for( const TopoDS_Shape& brdShape : m_board_outlines )
2628 BRepBndLib::Add( brdShape, brdBndBox );
2629
2630 // subtract cutouts (if any)
2631 m_reporter->Report( wxString::Format( wxT( "Build board cutouts and holes (%d hole(s))." ),
2632 (int) ( m_boardCutouts.size() + m_copperCutouts.size() ) ),
2634
2635 auto buildBSB =
2636 [&brdBndBox]( std::vector<TopoDS_Shape>& input, Bnd_BoundSortBox& bsbHoles,
2637 std::vector<Bnd_Box>& holeBoxes )
2638 {
2639 // We need to encompass every location we'll need to test in the global bbox,
2640 // otherwise Bnd_BoundSortBox doesn't work near the boundaries.
2641 Bnd_Box brdWithHolesBndBox = brdBndBox;
2642
2643 Handle( Bnd_HArray1OfBox ) holeBoxSet = new Bnd_HArray1OfBox( 0, input.size() - 1 );
2644 holeBoxes.resize( input.size() );
2645
2646 for( size_t i = 0; i < input.size(); i++ )
2647 {
2648 Bnd_Box bbox;
2649 BRepBndLib::Add( input[i], bbox );
2650 brdWithHolesBndBox.Add( bbox );
2651 ( *holeBoxSet )[i] = bbox;
2652 holeBoxes[i] = bbox;
2653 }
2654
2655 bsbHoles.Initialize( brdWithHolesBndBox, holeBoxSet );
2656 };
2657
2658 auto subtractShapesMap =
2659 [this, &tp]( const wxString& aWhat, std::map<wxString, std::vector<TopoDS_Shape>>& aShapesMap,
2660 std::vector<TopoDS_Shape>& aHolesList, Bnd_BoundSortBox& aBSBHoles,
2661 const std::vector<Bnd_Box>& aHoleBoxes )
2662 {
2663 m_reporter->Report( wxString::Format( _( "Subtracting holes for %s" ), aWhat ),
2665
2666 for( auto& [netname, vec] : aShapesMap )
2667 {
2668 // Cuts share the hole TShapes as tools across threads. SetNonDestructive keeps
2669 // OCC from mutating those shared inputs, so the cuts are safe to run in parallel.
2670 // Bnd_BoundSortBox::Compare is not reentrant (it overwrites internal scratch and
2671 // returns a reference to it), so the hole lookup is serialized with a mutex.
2672 std::mutex mutex;
2673
2674 auto subtractLoopFn = [&]( const int shapeId )
2675 {
2676 TopoDS_Shape& shape = vec[shapeId];
2677
2678 Bnd_Box shapeBbox;
2679 BRepBndLib::Add( shape, shapeBbox );
2680
2681 NCollection_List<TopoDS_Shape> holelist;
2682
2683 {
2684 std::unique_lock lock( mutex );
2685
2686 const NCollection_List<int>& indices = aBSBHoles.Compare( shapeBbox );
2687
2688 for( const int& index : indices )
2689 holelist.Append( aHolesList[index] );
2690
2691 // Workaround for OCCT bug (https://github.com/Open-Cascade-SAS/OCCT/issues/506)
2692 // Bnd_BoundSortBox::Compare can fail to detect intersections in certain edge
2693 // cases (e.g., single item). Fall back to direct bounding box intersection
2694 // checks when Compare returns empty but intersections may exist.
2695 if( holelist.IsEmpty() )
2696 {
2697 for( size_t i = 0; i < aHoleBoxes.size(); i++ )
2698 {
2699 if( !shapeBbox.IsOut( aHoleBoxes[i] ) )
2700 holelist.Append( aHolesList[i] );
2701 }
2702 }
2703 }
2704
2705 if( holelist.IsEmpty() )
2706 return; // nothing to cut for this shape
2707
2708 NCollection_List<TopoDS_Shape> cutArgs;
2709 cutArgs.Append( shape );
2710
2711 BRepAlgoAPI_Cut cut;
2712
2713 // Non-destructive protects the shared hole tools. Parallelism comes from the
2714 // outer thread pool, so this op runs single-threaded to avoid oversubscribing.
2715 cut.SetNonDestructive( true );
2716 cut.SetRunParallel( false );
2717 cut.SetToFillHistory( false );
2718
2719 cut.SetArguments( cutArgs );
2720 cut.SetTools( holelist );
2721 cut.Build();
2722
2723 if( cut.HasErrors() || cut.HasWarnings() )
2724 {
2725 m_reporter->Report( wxString::Format( _( "** Got problems while cutting "
2726 "%s net '%s' **" ),
2727 aWhat,
2728 UnescapeString( netname ) ),
2730
2731 {
2732 // Dump writes to std::cout; serialize it so parallel cuts do not
2733 // interleave their output.
2734 std::unique_lock lock( mutex );
2735 shapeBbox.Dump();
2736 }
2737
2738 if( cut.HasErrors() )
2739 {
2740 wxString msg = _( "Errors:\n" );
2741 wxStringOutputStream os_stream( &msg );
2742 wxStdOutputStream out( os_stream );
2743
2744 cut.DumpErrors( out );
2745 m_reporter->Report( msg, RPT_SEVERITY_WARNING);
2746 }
2747
2748 if( cut.HasWarnings() )
2749 {
2750 wxString msg = _( "Warnings:\n" );
2751 wxStringOutputStream os_stream( &msg );
2752 wxStdOutputStream out( os_stream );
2753
2754 cut.DumpWarnings( out );
2755 m_reporter->Report( msg, RPT_SEVERITY_WARNING );
2756 }
2757 }
2758
2759 shape = cut.Shape();
2760 };
2761
2762 // submit_loop can throw mid-submission after queueing some blocks. Drain the
2763 // pool before unwinding so no queued block outlives the captured mutex and
2764 // vector. get() then re-raises any worker exception.
2765 BS::multi_future<void> cutFutures;
2766
2767 try
2768 {
2769 cutFutures = tp.submit_loop( 0, vec.size(), subtractLoopFn );
2770 }
2771 catch( ... )
2772 {
2773 tp.wait();
2774 throw;
2775 }
2776
2777 cutFutures.wait();
2778 cutFutures.get();
2779 }
2780 };
2781
2782 auto subtractShapes =
2783 [subtractShapesMap]( const wxString& aWhat, std::vector<TopoDS_Shape>& aShapesList,
2784 std::vector<TopoDS_Shape>& aHolesList, Bnd_BoundSortBox& aBSBHoles,
2785 const std::vector<Bnd_Box>& aHoleBoxes )
2786 {
2787 std::map<wxString, std::vector<TopoDS_Shape>> aShapesMap{ { wxEmptyString, aShapesList } };
2788
2789 subtractShapesMap( aWhat, aShapesMap, aHolesList, aBSBHoles, aHoleBoxes );
2790 aShapesList = aShapesMap[wxEmptyString];
2791 };
2792
2793
2794 if( m_boardCutouts.size() )
2795 {
2796 Bnd_BoundSortBox bsbHoles;
2797 std::vector<Bnd_Box> holeBoxes;
2798 buildBSB( m_boardCutouts, bsbHoles, holeBoxes );
2799
2800 subtractShapes( _( "shapes" ), m_board_outlines, m_boardCutouts, bsbHoles, holeBoxes );
2801 }
2802
2803 if( m_copperCutouts.size() )
2804 {
2805 Bnd_BoundSortBox bsbHoles;
2806 std::vector<Bnd_Box> holeBoxes;
2807 buildBSB( m_copperCutouts, bsbHoles, holeBoxes );
2808
2809 subtractShapesMap( _( "pads" ), m_board_copper_pads, m_copperCutouts, bsbHoles, holeBoxes );
2810 subtractShapesMap( _( "vias" ), m_board_copper_vias, m_copperCutouts, bsbHoles, holeBoxes );
2811 }
2812
2813 if( m_fuseShapes )
2814 {
2815 std::map<wxString, NCollection_List<TopoDS_Shape>> shapesToFuseMap;
2816
2817 auto addShapes = [&shapesToFuseMap]( const wxString& aNetname,
2818 const std::vector<TopoDS_Shape>& aShapes )
2819 {
2820 for( const TopoDS_Shape& shape : aShapes )
2821 shapesToFuseMap[aNetname].Append( shape );
2822 };
2823
2824 for( const auto& [netname, shapes] : m_board_copper )
2825 addShapes( netname, shapes );
2826
2827 for( const auto& [netname, shapes] : m_board_copper_pads )
2828 addShapes( netname, shapes );
2829
2830 for( const auto& [netname, shapes] : m_board_copper_vias )
2831 addShapes( netname, shapes );
2832
2833 m_reporter->Report( wxT( "Fusing shapes" ), RPT_SEVERITY_DEBUG );
2834
2835 // Fuse each net on the thread pool. SetNonDestructive keeps the shared input TShapes
2836 // immutable, so the parallel BRepAlgoAPI ops are heap-safe. The member-map writes are the
2837 // only shared mutable state and are guarded by the mutex. Work items hold stable pointers
2838 // into shapesToFuseMap so the workers never touch the map's non-const operator[].
2839 std::vector<std::pair<wxString, const NCollection_List<TopoDS_Shape>*>> fuseWork;
2840 fuseWork.reserve( shapesToFuseMap.size() );
2841
2842 for( const auto& [netname, toFuse] : shapesToFuseMap )
2843 fuseWork.emplace_back( netname, &toFuse );
2844
2845 std::mutex mutex;
2846 BS::multi_future<void> mf;
2847 mf.reserve( fuseWork.size() );
2848
2849 // A submission can throw after queueing tasks (allocation failure). Drain the pool before
2850 // unwinding so no task outlives the captured mutex.
2851 try
2852 {
2853 for( const auto& work : fuseWork )
2854 {
2855 const wxString netname = work.first;
2856 const NCollection_List<TopoDS_Shape>* toFuse = work.second;
2857
2858 mf.push_back( tp.submit_task(
2859 [this, &mutex, netname, toFuse]()
2860 {
2861 TopoDS_Shape fusedShape = fuseShapesOrCompound( *toFuse, m_reporter );
2862
2863 if( !fusedShape.IsNull() )
2864 {
2865 std::unique_lock lock( mutex );
2866
2867 m_board_copper_fused[netname].emplace_back( fusedShape );
2868
2869 m_board_copper[netname].clear();
2870 m_board_copper_pads[netname].clear();
2871 m_board_copper_vias[netname].clear();
2872 }
2873 } ) );
2874 }
2875 }
2876 catch( ... )
2877 {
2878 tp.wait();
2879 throw;
2880 }
2881
2882 mf.wait();
2883 mf.get();
2884 }
2885
2886 // push the board to the data structure
2887 m_reporter->Report( wxT( "Generate board full shape." ), RPT_SEVERITY_DEBUG );
2888
2889 // AddComponent adds a label that has a reference (not a parent/child relation) to the real
2890 // label. We need to extract that real label to name it for the STEP output cleanly
2891 // Why are we trying to name the bare board? Because CAD tools like SolidWorks do fun things
2892 // like "deduplicate" imported STEPs by swapping STEP assembly components with already
2893 // identically named assemblies. So we want to avoid having the PCB be generally defaulted
2894 // to "Component" or "Assembly".
2895
2896 // aCompoundNets will place all geometry within a net into one compound.
2897 // aCompoundAll will place all geometry into one compound.
2898 auto pushToAssemblyMap =
2899 [&]( const std::map<wxString, std::vector<TopoDS_Shape>>& aShapesMap,
2900 const TDF_Label& aVisMatLabel, const wxString& aShapeName, bool aCompoundNets,
2901 bool aCompoundAll, const wxString& aNiceName )
2902 {
2903 std::map<wxString, std::vector<TopoDS_Shape>> shapesMap;
2904
2905 if( aCompoundAll )
2906 {
2907 std::vector<TopoDS_Shape> allShapes;
2908
2909 for( const auto& [netname, shapesList] : aShapesMap )
2910 allShapes.insert( allShapes.end(), shapesList.begin(), shapesList.end() );
2911
2912 if( !allShapes.empty() )
2913 shapesMap[wxEmptyString].emplace_back( makeCompound( allShapes ) );
2914 }
2915 else
2916 {
2917 shapesMap = aShapesMap;
2918 }
2919
2920 for( const auto& [netname, shapesList] : shapesMap )
2921 {
2922 std::vector<TopoDS_Shape> newList;
2923
2924 if( aCompoundNets )
2925 newList.emplace_back( makeCompound( shapesList ) );
2926 else
2927 newList = shapesList;
2928
2929 int i = 1;
2930
2931 for( TopoDS_Shape& shape : newList )
2932 {
2933 Handle( TDataStd_TreeNode ) node;
2934
2935 // Dont expand the component or else coloring it gets hard
2936 TDF_Label lbl = m_assy->AddComponent( m_assy_label, shape, false );
2937 KICAD3D_INFO::Set( lbl, KICAD3D_MODEL_TYPE::BOARD, aNiceName.ToStdString() );
2938 m_pcb_labels.push_back( lbl );
2939
2940 if( m_pcb_labels.back().IsNull() )
2941 return;
2942
2943 lbl.FindAttribute( XCAFDoc::ShapeRefGUID(), node );
2944 TDF_Label shpLbl = node->Father()->Label();
2945
2946 if( !shpLbl.IsNull() )
2947 {
2948 if( visMatTool && !aVisMatLabel.IsNull() )
2949 visMatTool->SetShapeMaterial( shpLbl, aVisMatLabel );
2950
2951 wxString shapeName;
2952
2953 shapeName << m_pcbName;
2954 shapeName << '_';
2955 shapeName << aShapeName;
2956
2957 if( !netname.empty() )
2958 {
2959 shapeName << '_';
2960 shapeName << netname;
2961 }
2962
2963 if( newList.size() > 1 )
2964 {
2965 shapeName << '_';
2966 shapeName << i;
2967 }
2968
2969 TCollection_ExtendedString partname( shapeName.ToUTF8().data() );
2970 TDataStd_Name::Set( shpLbl, partname );
2971 }
2972
2973 i++;
2974 }
2975 }
2976 };
2977
2978 auto pushToAssembly =
2979 [&]( const std::vector<TopoDS_Shape>& aShapesList, const TDF_Label& aVisMatLabel,
2980 const wxString& aShapeName, bool aCompound, const wxString& aNiceName )
2981 {
2982 const std::map<wxString, std::vector<TopoDS_Shape>> shapesMap{ { wxEmptyString, aShapesList } };
2983
2984 pushToAssemblyMap( shapesMap, aVisMatLabel, aShapeName, aCompound, aCompound, aNiceName );
2985 };
2986
2987 auto makeMaterial =
2988 [&]( const TCollection_AsciiString& aName, const Quantity_ColorRGBA& aBaseColor,
2989 double aMetallic, double aRoughness ) -> TDF_Label
2990 {
2991 Handle( XCAFDoc_VisMaterial ) vismat = new XCAFDoc_VisMaterial;
2992 XCAFDoc_VisMaterialPBR pbr;
2993 pbr.BaseColor = aBaseColor;
2994 pbr.Metallic = aMetallic;
2995 pbr.Roughness = aRoughness;
2996 vismat->SetPbrMaterial( pbr );
2997 return visMatTool->AddMaterial( vismat, aName );
2998 };
2999
3000 // Init colors for the board items
3001 Quantity_ColorRGBA copper_color( m_copperColor[0], m_copperColor[1], m_copperColor[2], 1.0 );
3002 Quantity_ColorRGBA pad_color( m_padColor[0], m_padColor[1], m_padColor[2], 1.0 );
3003
3004 Quantity_ColorRGBA board_color( 0.42f, 0.45f, 0.29f, 0.98f );
3005 Quantity_ColorRGBA front_silk_color( 1.0f, 1.0f, 1.0f, 0.9f );
3006 Quantity_ColorRGBA back_silk_color = front_silk_color;
3007 Quantity_ColorRGBA front_mask_color( 0.08f, 0.2f, 0.14f, 0.83f );
3008 Quantity_ColorRGBA back_mask_color = front_mask_color;
3009
3010 // Get colors from stackup
3011 for( const BOARD_STACKUP_ITEM* item : m_stackup.GetList() )
3012 {
3013 COLOR4D col;
3014
3015 if( !colorFromStackup( item->GetType(), item->GetColor(), col ) )
3016 continue;
3017
3018 if( item->GetBrdLayerId() == F_Mask || item->GetBrdLayerId() == B_Mask )
3019 {
3020 col.Darken( 0.2 );
3021
3022 if( item->GetBrdLayerId() == F_Mask )
3023 front_mask_color.SetValues( col.r, col.g, col.b, col.a );
3024 else
3025 back_mask_color.SetValues( col.r, col.g, col.b, col.a );
3026 }
3027
3028 if( item->GetBrdLayerId() == F_SilkS )
3029 front_silk_color.SetValues( col.r, col.g, col.b, col.a );
3030 else if( item->GetBrdLayerId() == B_SilkS )
3031 back_silk_color.SetValues( col.r, col.g, col.b, col.a );
3032
3033 if( item->GetType() == BS_ITEM_TYPE_DIELECTRIC && item->GetTypeName() == KEY_CORE )
3034 board_color.SetValues( col.r, col.g, col.b, col.a );
3035 }
3036
3037 // Paint board body in soldermask colors if soldermask is not exported as a layer
3038 if( !m_enabledLayers.Contains( F_Mask ) && !m_enabledLayers.Contains( B_Mask ) )
3039 {
3040 board_color = front_mask_color;
3041 board_color.SetAlpha( 1.0 );
3042 }
3043
3044 TDF_Label front_mask_mat = makeMaterial( "soldermask", front_mask_color, 0.0, 0.6 );
3045 TDF_Label back_mask_mat = makeMaterial( "soldermask", back_mask_color, 0.0, 0.6 );
3046 TDF_Label front_silk_mat = makeMaterial( "silkscreen", front_silk_color, 0.0, 0.9 );
3047 TDF_Label back_silk_mat = makeMaterial( "silkscreen", back_silk_color, 0.0, 0.9 );
3048 TDF_Label copper_mat = makeMaterial( "copper", copper_color, 1.0, 0.4 );
3049 TDF_Label pad_mat = makeMaterial( "pad", pad_color, 1.0, 0.4 );
3050 TDF_Label board_mat = makeMaterial( "board", board_color, 0.0, 0.8 );
3051
3052 pushToAssemblyMap( m_board_copper, copper_mat, "copper", true, true, "Copper" );
3053 pushToAssemblyMap( m_board_copper_pads, pad_mat, "pad", true, true, "Pads" );
3054 pushToAssemblyMap( m_board_copper_vias, copper_mat, "via", true, true, "Via" );
3055 pushToAssemblyMap( m_board_copper_fused, copper_mat, "copper", true, true, "Copper" );
3056 pushToAssembly( m_board_front_silk, front_silk_mat, "silkscreen", true, "Top Silkscreen" );
3057 pushToAssembly( m_board_back_silk, back_silk_mat, "silkscreen", true, "Bottom Silkscreen" );
3058 pushToAssembly( m_board_front_mask, front_mask_mat, "soldermask", true, "Top Soldermask" );
3059 pushToAssembly( m_board_back_mask, back_mask_mat, "soldermask", true, "Bottom Soldermask" );
3060
3061 if( aPushBoardBody )
3062 pushToAssembly( m_board_outlines, board_mat, "PCB", false, "Body" );
3063
3064 Quantity_ColorRGBA pinClr( Quantity_Color( 0.75, 0.75, 0.75, Quantity_TOC_RGB ), 1.0 );
3065 TDF_Label pin_mat = makeMaterial( "extruded_pin", pinClr, 0.6, 0.3 );
3066
3067 for( auto& entry : m_extruded_bodies )
3068 {
3069 if( entry.bodyShapes.empty() && entry.pinShapes.empty() )
3070 continue;
3071
3072 TopoDS_Compound asmCompound;
3073 BRep_Builder asmBuilder;
3074 asmBuilder.MakeCompound( asmCompound );
3075 TDF_Label fpLabel = m_assy->AddShape( asmCompound, true );
3076 TDataStd_Name::Set( fpLabel, TCollection_ExtendedString( ( entry.refDes + " (extruded)" ).ToUTF8().data() ) );
3077
3078 if( !entry.bodyShapes.empty() )
3079 {
3080 double r = ( ( entry.colorKey >> 24 ) & 0xFF ) / 255.0;
3081 double g = ( ( entry.colorKey >> 16 ) & 0xFF ) / 255.0;
3082 double b = ( ( entry.colorKey >> 8 ) & 0xFF ) / 255.0;
3083 double a = ( entry.colorKey & 0xFF ) / 255.0;
3084
3085 double metallic, roughness;
3086
3087 switch( entry.material )
3088 {
3089 default:
3091 metallic = 0.0;
3092 roughness = 0.6;
3093 break;
3095 metallic = 0.0;
3096 roughness = 0.9;
3097 break;
3099 metallic = 0.8;
3100 roughness = 0.3;
3101 break;
3103 metallic = 1.0;
3104 roughness = 0.4;
3105 break;
3106 }
3107
3108 Quantity_ColorRGBA bodyClr( Quantity_Color( r, g, b, Quantity_TOC_RGB ), a );
3109 TDF_Label body_mat = makeMaterial( "extruded_body", bodyClr, metallic, roughness );
3110
3111 TopoDS_Shape bodyCompound = makeCompound( entry.bodyShapes );
3112 TDF_Label bodyLbl = m_assy->AddComponent( fpLabel, bodyCompound, false );
3113
3114 Handle( TDataStd_TreeNode ) bodyNode;
3115 bodyLbl.FindAttribute( XCAFDoc::ShapeRefGUID(), bodyNode );
3116 TDF_Label bodyShpLbl = bodyNode->Father()->Label();
3117
3118 if( !bodyShpLbl.IsNull() )
3119 {
3120 visMatTool->SetShapeMaterial( bodyShpLbl, body_mat );
3121 TDataStd_Name::Set( bodyShpLbl,
3122 TCollection_ExtendedString( ( entry.refDes + "_body" ).ToUTF8().data() ) );
3123 }
3124 }
3125
3126 int pinIdx = 1;
3127
3128 for( TopoDS_Shape& pinShape : entry.pinShapes )
3129 {
3130 TDF_Label pinLbl = m_assy->AddComponent( fpLabel, pinShape, false );
3131
3132 Handle( TDataStd_TreeNode ) pinNode;
3133 pinLbl.FindAttribute( XCAFDoc::ShapeRefGUID(), pinNode );
3134 TDF_Label pinShpLbl = pinNode->Father()->Label();
3135
3136 if( !pinShpLbl.IsNull() )
3137 {
3138 visMatTool->SetShapeMaterial( pinShpLbl, pin_mat );
3139 wxString pinName = wxString::Format( "%s_pin_%d", entry.refDes, pinIdx++ );
3140 TDataStd_Name::Set( pinShpLbl, TCollection_ExtendedString( pinName.ToUTF8().data() ) );
3141 }
3142 }
3143
3144 TopLoc_Location loc;
3145 TDF_Label fpCompLbl = m_assy->AddComponent( m_assy_label, fpLabel, loc );
3146 TDataStd_Name::Set( fpCompLbl, TCollection_ExtendedString( entry.refDes.ToUTF8().data() ) );
3147 KICAD3D_INFO::Set( fpCompLbl, KICAD3D_MODEL_TYPE::BOARD, entry.refDes.ToStdString() );
3148 m_pcb_labels.push_back( fpCompLbl );
3149 }
3150
3151#if( defined OCC_VERSION_HEX ) && ( OCC_VERSION_HEX > 0x070101 )
3152 m_assy->UpdateAssemblies();
3153#endif
3154
3155 return true;
3156}
3157
3158
3159#ifdef SUPPORTS_IGES
3160// write the assembly model in IGES format
3161bool STEP_PCB_MODEL::WriteIGES( const wxString& aFileName )
3162{
3163 if( !isBoardOutlineValid() )
3164 {
3165 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
3166 aFileName ),
3168 return false;
3169 }
3170
3172
3173 wxFileName fn( aFileName );
3174 IGESControl_Controller::Init();
3175 IGESCAFControl_Writer writer;
3176 writer.SetColorMode( true );
3177 writer.SetNameMode( true );
3178 IGESData_GlobalSection header = writer.Model()->GlobalSection();
3179 header.SetFileName( new TCollection_HAsciiString( fn.GetFullName().ToAscii() ) );
3180 header.SetSendName( new TCollection_HAsciiString( "KiCad electronic assembly" ) );
3181 header.SetAuthorName( new TCollection_HAsciiString( Interface_Static::CVal( "write.iges.header.author" ) ) );
3182 header.SetCompanyName( new TCollection_HAsciiString( Interface_Static::CVal( "write.iges.header.company" ) ) );
3183 writer.Model()->SetGlobalSection( header );
3184
3185 if( false == writer.Perform( m_doc, aFileName.c_str() ) )
3186 return false;
3187
3188 return true;
3189}
3190#endif
3191
3192bool STEP_PCB_MODEL::CompressSTEP( wxString& inputFile, wxString& outputFile )
3193{
3194 wxFileInputStream input( inputFile );
3195 wxFileOutputStream output( outputFile );
3196
3197 if( !input.IsOk() )
3198 {
3199 m_reporter->Report( wxString::Format( _( "Cannot create input stream '%s'.\n" ), inputFile ) );
3200 return false;
3201 }
3202
3203 if( !output.IsOk() )
3204 {
3205 m_reporter->Report( wxString::Format( _( "Cannot create output stream '%s'.\n" ), outputFile ) );
3206 return false;
3207 }
3208
3209 wxZlibOutputStream zlibStream( output, -1, wxZLIB_GZIP );
3210
3211 if( !zlibStream.IsOk() )
3212 {
3213 m_reporter->Report( _( "Impossible create compress stream" ) );
3214 return false;
3215 }
3216
3217 input.Read( zlibStream );
3218
3219 if( input.LastRead() == 0 || zlibStream.LastWrite() == 0 )
3220 {
3221 m_reporter->Report( _( "Compress read or write error" ) );
3222 return false;
3223 }
3224
3225 zlibStream.Close();
3226 output.Close();
3227
3228 return true;
3229}
3230
3231bool STEP_PCB_MODEL::WriteSTEP( const wxString& aFileName, bool aOptimize, bool compress )
3232{
3233 if( !isBoardOutlineValid() )
3234 {
3235 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
3236 aFileName ),
3238 return false;
3239 }
3240
3242
3243 wxFileName fn( aFileName );
3244
3245 STEPCAFControl_Writer writer;
3246 writer.SetColorMode( true );
3247 writer.SetNameMode( true );
3248
3249 // This must be set before we "transfer" the document.
3250 // Should default to kicad_pcb.general.title_block.title,
3251 // but in the meantime, defaulting to the basename of the output
3252 // target is still better than "open cascade step translter v..."
3253 // UTF8 should be ok from ISO 10303-21:2016, but... older stuff? use boring ascii
3254 if( !Interface_Static::SetCVal( "write.step.product.name", fn.GetName().ToAscii() ) )
3255 {
3256 m_reporter->Report( _( "Failed to set STEP product name, but will attempt to continue." ),
3258 }
3259
3260 // Setting write.surfacecurve.mode to 0 reduces file size and write/read times.
3261 // But there are reports that this mode might be less compatible in some cases.
3262 if( !Interface_Static::SetIVal( "write.surfacecurve.mode", aOptimize ? 0 : 1 ) )
3263 {
3264 m_reporter->Report( _( "Failed to set surface curve mode, but will attempt to continue." ),
3266 }
3267
3268 if( false == writer.Transfer( m_doc, STEPControl_AsIs ) )
3269 return false;
3270
3271 APIHeaderSection_MakeHeader hdr( writer.ChangeWriter().Model() );
3272
3273 // Note: use only Ascii7 chars, non Ascii7 chars (therefore UFT8 chars)
3274 // are creating issues in the step file
3275 hdr.SetName( new TCollection_HAsciiString( fn.GetFullName().ToAscii() ) );
3276
3277 // TODO: how to control and ensure consistency with IGES?
3278 hdr.SetAuthorValue( 1, new TCollection_HAsciiString( "Pcbnew" ) );
3279 hdr.SetOrganizationValue( 1, new TCollection_HAsciiString( "Kicad" ) );
3280 hdr.SetOriginatingSystem( new TCollection_HAsciiString( "KiCad to STEP converter" ) );
3281 hdr.SetDescriptionValue( 1, new TCollection_HAsciiString( "KiCad electronic assembly" ) );
3282
3283 bool success = true;
3284
3285 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
3286 wxString currCWD = wxGetCwd();
3287 wxString workCWD = fn.GetPath();
3288
3289 if( !workCWD.IsEmpty() )
3290 wxSetWorkingDirectory( workCWD );
3291
3292 wxString tmpfname( "$tempfile$.step" );
3293
3294 if( false == writer.Write( tmpfname.c_str() ) )
3295 success = false;
3296
3297 if( compress && success )
3298 {
3299 wxString srcTmp( tmpfname );
3300 wxString dstTmp( "$tempfile$.stpz" );
3301
3302 success = STEP_PCB_MODEL::CompressSTEP( srcTmp, dstTmp );
3303 wxRemoveFile( srcTmp );
3304
3305 tmpfname = dstTmp;
3306 }
3307
3308 if( success )
3309 {
3310
3311 // Preserve the permissions of the current file
3312 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpfname.c_str() );
3313
3314 if( !wxRenameFile( tmpfname, fn.GetFullName(), true ) )
3315 {
3316 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'." ),
3317 tmpfname,
3318 fn.GetFullName() ),
3320 success = false;
3321 }
3322 }
3323
3324 wxSetWorkingDirectory( currCWD );
3325
3326 return success;
3327}
3328
3329
3330bool STEP_PCB_MODEL::WriteBREP( const wxString& aFileName )
3331{
3332 if( !isBoardOutlineValid() )
3333 {
3334 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
3335 aFileName ),
3337 return false;
3338 }
3339
3341
3342 // s_assy = shape tool for the source
3343 Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
3344
3345 // retrieve assembly as a single shape
3346 TopoDS_Shape shape = getOneShape( s_assy );
3347
3348 wxFileName fn( aFileName );
3349
3350 wxFFileOutputStream ffStream( fn.GetFullPath() );
3351 wxStdOutputStream stdStream( ffStream );
3352
3353#if OCC_VERSION_HEX >= 0x070600
3354 BRepTools::Write( shape, stdStream, false, false, TopTools_FormatVersion_VERSION_1 );
3355#else
3356 BRepTools::Write( shape, stdStream );
3357#endif
3358
3359 return true;
3360}
3361
3362
3363bool STEP_PCB_MODEL::WriteXAO( const wxString& aFileName )
3364{
3365 wxFileName fn( aFileName );
3366
3367 wxFFileOutputStream ffStream( fn.GetFullPath() );
3368 wxStdOutputStream file( ffStream );
3369
3370 if( !ffStream.IsOk() )
3371 {
3372 m_reporter->Report( wxString::Format( "Could not open file '%s'", fn.GetFullPath() ),
3374 return false;
3375 }
3376
3378
3379 // s_assy = shape tool for the source
3380 Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
3381
3382 // retrieve assembly as a single shape
3383 const TopoDS_Shape shape = getOneShape( s_assy );
3384
3385 std::map<wxString, std::vector<int>> groups[4];
3386 std::map<wxString, double> groupAreas;
3387 TopExp_Explorer exp;
3388 int faceIndex = 0;
3389
3390 for( exp.Init( shape, TopAbs_FACE ); exp.More(); exp.Next() )
3391 {
3392 TopoDS_Shape subShape = exp.Current();
3393
3394 Bnd_Box bbox;
3395 BRepBndLib::Add( subShape, bbox );
3396
3397 for( const auto& [padKey, pairs] : m_pad_points )
3398 {
3399 for( const auto& pair : pairs )
3400 {
3401 const auto& [point, padTestShape] = pair;
3402
3403 if( bbox.IsOut( point ) )
3404 continue;
3405
3406 BRepAdaptor_Surface surface( TopoDS::Face( subShape ) );
3407
3408 if( surface.GetType() != GeomAbs_Plane )
3409 continue;
3410
3411 BRepExtrema_DistShapeShape dist( padTestShape, subShape );
3412 dist.Perform();
3413
3414 if( !dist.IsDone() )
3415 continue;
3416
3417 if( dist.Value() < Precision::Approximation() )
3418 {
3419 // Push as a face group
3420 groups[2][padKey].push_back( faceIndex );
3421
3422 GProp_GProps system;
3423 BRepGProp::SurfaceProperties( subShape, system );
3424
3425 double surfaceArea = system.Mass() / 1e6; // Convert to meters^2
3426 groupAreas[padKey] += surfaceArea;
3427 }
3428 }
3429 }
3430
3431 faceIndex++;
3432 }
3433
3434 // Based on Gmsh code
3435 file << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << std::endl;
3436 file << "<XAO version=\"1.0\" author=\"KiCad\">" << std::endl;
3437 file << " <geometry name=\"" << fn.GetName() << "\">" << std::endl;
3438 file << " <shape format=\"BREP\"><![CDATA[";
3439#if OCC_VERSION_HEX < 0x070600
3440 BRepTools::Write( shape, file );
3441#else
3442 BRepTools::Write( shape, file, true, true, TopTools_FormatVersion_VERSION_1 );
3443#endif
3444 file << "]]></shape>" << std::endl;
3445 file << " <topology>" << std::endl;
3446
3447 TopTools_IndexedMapOfShape mainMap;
3448 TopExp::MapShapes( shape, mainMap );
3449 std::set<int> topo[4];
3450
3451 static const TopAbs_ShapeEnum c_dimShapeTypes[] = { TopAbs_VERTEX, TopAbs_EDGE, TopAbs_FACE,
3452 TopAbs_SOLID };
3453
3454 static const std::string c_dimLabel[] = { "vertex", "edge", "face", "solid" };
3455 static const std::string c_dimLabels[] = { "vertices", "edges", "faces", "solids" };
3456
3457 for( int dim = 0; dim < 4; dim++ )
3458 {
3459 for( exp.Init( shape, c_dimShapeTypes[dim] ); exp.More(); exp.Next() )
3460 {
3461 TopoDS_Shape subShape = exp.Current();
3462 int idx = mainMap.FindIndex( subShape );
3463
3464 if( idx && !topo[dim].count( idx ) )
3465 topo[dim].insert( idx );
3466 }
3467 }
3468
3469 for( int dim = 0; dim <= 3; dim++ )
3470 {
3471 std::string labels = c_dimLabels[dim];
3472 std::string label = c_dimLabel[dim];
3473
3474 file << " <" << labels << " count=\"" << topo[dim].size() << "\">" << std::endl;
3475 int index = 0;
3476
3477 for( auto p : topo[dim] )
3478 {
3479 std::string name( "" );
3480 file << " <" << label << " index=\"" << index << "\" "
3481 << "name=\"" << name << "\" "
3482 << "reference=\"" << p << "\"/>" << std::endl;
3483
3484 index++;
3485 }
3486 file << " </" << labels << ">" << std::endl;
3487 }
3488
3489 file << " </topology>" << std::endl;
3490 file << " </geometry>" << std::endl;
3491 file << " <groups count=\""
3492 << groups[0].size() + groups[1].size() + groups[2].size() + groups[3].size() << "\">"
3493 << std::endl;
3494
3495 int groupNumber = 1;
3496
3497 m_reporter->Report( wxT( "Pad definitions:" ), RPT_SEVERITY_DEBUG );
3498 m_reporter->Report( wxT( "Number\tName\tArea (m^2)" ), RPT_SEVERITY_DEBUG );
3499
3500 for( int dim = 0; dim <= 3; dim++ )
3501 {
3502 std::string label = c_dimLabel[dim];
3503
3504 for( auto g : groups[dim] )
3505 {
3506 //std::string name = model->getPhysicalName( dim, g.first );
3507 wxString name = g.first;
3508
3509 if( name.empty() )
3510 { // create same unique name as for MED export
3511 std::ostringstream gs;
3512 gs << "G_" << dim << "D_" << g.first;
3513 name = gs.str();
3514 }
3515 file << " <group name=\"" << name << "\" dimension=\"" << label;
3516//#if 1
3517// // Gmsh XAO extension: also save the physical tag, so that XAO can be used
3518// // to serialize OCC geometries, ready to be used by GetDP, GmshFEM & co
3519// file << "\" tag=\"" << g.first;
3520//#endif
3521 file << "\" count=\"" << g.second.size() << "\">" << std::endl;
3522
3523 for( auto index : g.second )
3524 file << " <element index=\"" << index << "\"/>" << std::endl;
3525
3526 file << " </group>" << std::endl;
3527
3528 m_reporter->Report( wxString::Format( "%d\t%s\t%g",
3529 groupNumber,
3530 name,
3531 groupAreas[name] ),
3533
3534 groupNumber++;
3535 }
3536 }
3537
3538 m_reporter->Report( wxT( "" ), RPT_SEVERITY_DEBUG );
3539
3540 file << " </groups>" << std::endl;
3541 file << " <fields count=\"0\"/>" << std::endl;
3542 file << "</XAO>" << std::endl;
3543
3544 return true;
3545}
3546
3547
3548bool STEP_PCB_MODEL::getModelLabel( const wxString& aBaseName, const wxString& aFileName,
3549 const std::vector<wxString>& aAltFilenames, VECTOR3D aScale,
3550 TDF_Label& aLabel, bool aSubstituteModels,
3551 wxString* aErrorMessage )
3552{
3553 std::string fileNameUTF8 = aFileName.utf8_string();
3554
3555 std::string model_key = fileNameUTF8 + "_" + std::to_string( aScale.x ) + "_"
3556 + std::to_string( aScale.y ) + "_" + std::to_string( aScale.z );
3557
3558 MODEL_MAP::const_iterator mm = m_models.find( model_key );
3559
3560 if( mm != m_models.end() )
3561 {
3562 aLabel = mm->second;
3563 return true;
3564 }
3565
3566 aLabel.Nullify();
3567
3568 Handle( TDocStd_Document ) doc;
3569 m_app->NewDocument( "MDTV-XCAF", doc );
3570
3571 MODEL3D_FORMAT_TYPE modelFmt = fileType( fileNameUTF8.c_str() );
3572 TCollection_ExtendedString partname( aBaseName.utf8_str() );
3573
3574 switch( modelFmt )
3575 {
3576 case FMT_IGES:
3577 if( !readIGES( doc, fileNameUTF8.c_str() ) )
3578 {
3579 m_reporter->Report( wxString::Format( wxT( "readIGES() failed on filename '%s'." ), aFileName ),
3581 return false;
3582 }
3583
3584 break;
3585
3586 case FMT_STEP:
3587 if( !readSTEP( doc, fileNameUTF8.c_str() ) )
3588 {
3589 m_reporter->Report( wxString::Format( wxT( "readSTEP() failed on filename '%s'." ), aFileName ),
3591 return false;
3592 }
3593
3594 break;
3595
3596 case FMT_STEPZ:
3597 {
3598 // To export a compressed step file (.stpz or .stp.gz file), the best way is to
3599 // decaompress it in a temporaty file and load this temporary file
3600 wxFFileInputStream ifile( aFileName );
3601 wxFileName outFile( aFileName );
3602
3603 outFile.SetPath( wxStandardPaths::Get().GetTempDir() );
3604 outFile.SetExt( wxT( "step" ) );
3605 wxFileOffset size = ifile.GetLength();
3606
3607 if( size == wxInvalidOffset )
3608 {
3609 m_reporter->Report( wxString::Format( wxT( "getModelLabel() failed on filename '%s'." ),
3610 aFileName ),
3612 return false;
3613 }
3614
3615 {
3616 bool success = false;
3617
3618 {
3619 wxFFileOutputStream ofile( outFile.GetFullPath() );
3620
3621 if( !ofile.IsOk() )
3622 return false;
3623
3624 char* buffer = new char[size];
3625
3626 ifile.Read( buffer, size );
3627 std::string expanded;
3628
3629 try
3630 {
3631 expanded = gzip::decompress( buffer, size );
3632 success = true;
3633 }
3634 catch( ... )
3635 {
3636 // ignore - we try unzipping it below
3637 }
3638
3639 if( expanded.empty() )
3640 {
3641 ifile.Reset();
3642 ifile.SeekI( 0 );
3643 wxZipInputStream izipfile( ifile );
3644 std::unique_ptr<wxZipEntry> zip_file( izipfile.GetNextEntry() );
3645
3646 if( zip_file && !zip_file->IsDir() && izipfile.CanRead() )
3647 {
3648 izipfile.Read( ofile );
3649 success = true;
3650 }
3651 else
3652 {
3653 m_reporter->Report( wxString::Format( wxT( "failed to decompress '%s'." ), aFileName ),
3655 }
3656 }
3657 else
3658 {
3659 ofile.Write( expanded.data(), expanded.size() );
3660 }
3661
3662 delete[] buffer;
3663 }
3664
3665 if( success )
3666 {
3667 success = getModelLabel( aBaseName, outFile.GetFullPath(), aAltFilenames,
3668 VECTOR3D( 1.0, 1.0, 1.0 ), aLabel, false );
3669 }
3670
3671 return success;
3672 }
3673
3674 break;
3675 }
3676
3677 case FMT_WRL:
3678 case FMT_WRZ:
3679 /* WRL files are preferred for internal rendering, due to superior material properties, etc.
3680 * However they are not suitable for MCAD export.
3681 *
3682 * If a .wrl file is specified, attempt to locate a replacement file for it.
3683 *
3684 * If a valid replacement file is found, the label for THAT file will be associated with
3685 * the .wrl file
3686 */
3687 if( aSubstituteModels )
3688 {
3689 wxFileName wrlName( aFileName );
3690
3691 wxString basePath = wrlName.GetPath();
3692 wxString baseName = wrlName.GetName();
3693
3694 // List of alternate files to look for
3695 // Given in order of preference
3696 // (Break if match is found)
3697 wxArrayString alts;
3698
3699 // Step files
3700 alts.Add( wxT( "stp" ) );
3701 alts.Add( wxT( "step" ) );
3702 alts.Add( wxT( "STP" ) );
3703 alts.Add( wxT( "STEP" ) );
3704 alts.Add( wxT( "Stp" ) );
3705 alts.Add( wxT( "Step" ) );
3706 alts.Add( wxT( "stpz" ) );
3707 alts.Add( wxT( "stpZ" ) );
3708 alts.Add( wxT( "STPZ" ) );
3709 alts.Add( wxT( "step.gz" ) );
3710 alts.Add( wxT( "stp.gz" ) );
3711
3712 // IGES files
3713 alts.Add( wxT( "iges" ) );
3714 alts.Add( wxT( "IGES" ) );
3715 alts.Add( wxT( "igs" ) );
3716 alts.Add( wxT( "IGS" ) );
3717
3718 //TODO - Other alternative formats?
3719
3720 for( const auto& altExt : alts )
3721 {
3722 wxFileName altFile;
3723
3724 if( !aAltFilenames.empty() )
3725 {
3726 for( const wxString& altPath : aAltFilenames )
3727 {
3728 wxFileName iterFn( altPath );
3729
3730 if( iterFn.GetExt() == altExt )
3731 {
3732 altFile = iterFn;
3733 break;
3734 }
3735 }
3736 }
3737 else
3738 {
3739 altFile = wxFileName( basePath, baseName + wxT( "." ) + altExt );
3740 }
3741
3742 if( altFile.IsOk() && altFile.FileExists() )
3743 {
3744 // When substituting a STEP/IGS file for VRML, do not apply the VRML scaling
3745 // to the new STEP model. This process of auto-substitution is janky as all
3746 // heck so let's not mix up un-displayed scale factors with potentially
3747 // mis-matched files. And hope that the user doesn't have multiples files
3748 // named "model.wrl" and "model.stp" referring to different parts.
3749 // TODO: Fix model handling in v7. Default models should only be STP.
3750 // Have option to override this in DISPLAY.
3751 if( getModelLabel( aBaseName, altFile.GetFullPath(), {},
3752 VECTOR3D( 1.0, 1.0, 1.0 ), aLabel, false ) )
3753 {
3754 return true;
3755 }
3756 }
3757 }
3758 }
3759
3760 // VRML models only work when exporting to mesh formats
3761 // Also OCCT < 7.9.0 fails to load most VRML 2.0 models because of Switch nodes
3765 {
3766 if( readVRML( doc, fileNameUTF8.c_str() ) )
3767 {
3768 Handle( XCAFDoc_ShapeTool ) shapeTool =
3769 XCAFDoc_DocumentTool::ShapeTool( doc->Main() );
3770
3771 prefixNames( shapeTool->Label(), partname );
3772 }
3773 else
3774 {
3775 m_reporter->Report(
3776 wxString::Format( wxT( "readVRML() failed on filename '%s'." ),
3777 aFileName ),
3779
3780 return false;
3781 }
3782 }
3783 else // Substitution is not allowed
3784 {
3785 if( aErrorMessage )
3786 aErrorMessage->Printf( _( "Cannot use VRML models when exporting to non-mesh formats." ) );
3787
3788 return false;
3789 }
3790
3791 break;
3792
3793 // TODO: implement IDF and EMN converters
3794
3795 default:
3796 m_reporter->Report( wxString::Format( _( "Cannot identify actual file type for '%s'." ), aFileName ),
3798 return false;
3799 }
3800
3801 aLabel = transferModel( doc, m_doc, aScale );
3802
3803 if( aLabel.IsNull() )
3804 {
3805 m_reporter->Report( wxString::Format( _( "Could not transfer model data from file '%s'." ), aFileName ),
3807 return false;
3808 }
3809
3810 // attach the PART NAME ( base filename: note that in principle
3811 // different models may have the same base filename )
3812 TDataStd_Name::Set( aLabel, partname );
3813
3814 m_models.insert( MODEL_DATUM( model_key, aLabel ) );
3815 ++m_components;
3816 return true;
3817}
3818
3819
3820bool STEP_PCB_MODEL::getModelLocation( bool aBottom, const VECTOR2D& aPosition, double aRotation,
3821 const VECTOR3D& aOffset, const VECTOR3D& aOrientation,
3822 TopLoc_Location& aLocation )
3823{
3824 // Order of operations:
3825 // a. aOrientation is applied -Z*-Y*-X
3826 // b. aOffset is applied
3827 // Top ? add thickness to the Z offset
3828 // c. Bottom ? Rotate on X axis (in contrast to most ECAD which mirror on Y),
3829 // then rotate on +Z
3830 // Top ? rotate on -Z
3831 // d. aPosition is applied
3832 //
3833 // Note: Y axis is inverted in KiCad
3834
3835 gp_Trsf lPos;
3836 lPos.SetTranslation( gp_Vec( aPosition.x, -aPosition.y, 0.0 ) );
3837
3838 // Offset board thickness
3839 VECTOR3D offset( aOffset );
3840 offset.z += BOARD_OFFSET;
3841
3842 double boardThickness;
3843 double boardZPos;
3844 getBoardBodyZPlacement( boardZPos, boardThickness );
3845 double top = std::max( boardZPos, boardZPos + boardThickness );
3846 double bottom = std::min( boardZPos, boardZPos + boardThickness );
3847
3848 // 3D step models are placed on the top of copper layers.
3849 // This is true for SMD shapes, and perhaps not always true for TH shapes,
3850 // but we use this Z position for any 3D shape.
3851 double f_pos, f_thickness;
3852 getLayerZPlacement( F_Cu, f_pos, f_thickness );
3853 top += f_thickness;
3854 getLayerZPlacement( B_Cu, f_pos, f_thickness );
3855 bottom += f_thickness; // f_thickness is < 0 for B_Cu layer
3856
3857 gp_Trsf lRot;
3858
3859 if( aBottom )
3860 {
3861 offset.z -= bottom;
3862 lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), aRotation );
3863 lPos.Multiply( lRot );
3864 lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 1.0, 0.0, 0.0 ) ), M_PI );
3865 lPos.Multiply( lRot );
3866 }
3867 else
3868 {
3869 offset.z += top;
3870 lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), aRotation );
3871 lPos.Multiply( lRot );
3872 }
3873
3874 gp_Trsf lOff;
3875 lOff.SetTranslation( gp_Vec( offset.x, offset.y, offset.z ) );
3876 lPos.Multiply( lOff );
3877
3878 gp_Trsf lOrient;
3879 lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), -aOrientation.z );
3880 lPos.Multiply( lOrient );
3881 lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 1.0, 0.0 ) ), -aOrientation.y );
3882 lPos.Multiply( lOrient );
3883 lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 1.0, 0.0, 0.0 ) ), -aOrientation.x );
3884 lPos.Multiply( lOrient );
3885
3886 aLocation = TopLoc_Location( lPos );
3887 return true;
3888}
3889
3890
3891bool STEP_PCB_MODEL::readIGES( Handle( TDocStd_Document )& doc, const char* fname )
3892{
3893 IGESControl_Controller::Init();
3894 IGESCAFControl_Reader reader;
3895 IFSelect_ReturnStatus stat = reader.ReadFile( fname );
3896
3897 if( stat != IFSelect_RetDone )
3898 return false;
3899
3900 // Enable user-defined shape precision
3901 if( !Interface_Static::SetIVal( "read.precision.mode", 1 ) )
3902 return false;
3903
3904 // Set the shape conversion precision to USER_PREC (default 0.0001 has too many triangles)
3905 if( !Interface_Static::SetRVal( "read.precision.val", USER_PREC ) )
3906 return false;
3907
3908 // set other translation options
3909 reader.SetColorMode( true ); // use model colors
3910 reader.SetNameMode( false ); // don't use IGES label names
3911 reader.SetLayerMode( false ); // ignore LAYER data
3912
3913 if( !reader.Transfer( doc ) )
3914 {
3915 if( doc->CanClose() == CDM_CCS_OK )
3916 doc->Close();
3917
3918 return false;
3919 }
3920
3921 // are there any shapes to translate?
3922 if( reader.NbShapes() < 1 )
3923 {
3924 if( doc->CanClose() == CDM_CCS_OK )
3925 doc->Close();
3926
3927 return false;
3928 }
3929
3930 return true;
3931}
3932
3933
3934bool STEP_PCB_MODEL::readSTEP( Handle( TDocStd_Document )& doc, const char* fname )
3935{
3936 STEPCAFControl_Reader reader;
3937 IFSelect_ReturnStatus stat = reader.ReadFile( fname );
3938
3939 if( stat != IFSelect_RetDone )
3940 return false;
3941
3942 // Enable user-defined shape precision
3943 if( !Interface_Static::SetIVal( "read.precision.mode", 1 ) )
3944 return false;
3945
3946 // Set the shape conversion precision to USER_PREC (default 0.0001 has too many triangles)
3947 if( !Interface_Static::SetRVal( "read.precision.val", USER_PREC ) )
3948 return false;
3949
3950 // set other translation options
3951 reader.SetColorMode( true ); // use model colors
3952 reader.SetNameMode( true ); // use label names
3953 reader.SetLayerMode( false ); // ignore LAYER data
3954
3955 if( !reader.Transfer( doc ) )
3956 {
3957 if( doc->CanClose() == CDM_CCS_OK )
3958 doc->Close();
3959
3960 return false;
3961 }
3962
3963 // are there any shapes to translate?
3964 if( reader.NbRootsForTransfer() < 1 )
3965 {
3966 if( doc->CanClose() == CDM_CCS_OK )
3967 doc->Close();
3968
3969 return false;
3970 }
3971
3972 return true;
3973}
3974
3975
3976bool STEP_PCB_MODEL::readVRML( Handle( TDocStd_Document ) & doc, const char* fname )
3977{
3978#if OCC_VERSION_HEX >= 0x070700
3979 VrmlAPI_CafReader reader;
3980 RWMesh_CoordinateSystemConverter conv;
3981 conv.SetInputLengthUnit( 2.54 );
3982 reader.SetCoordinateSystemConverter( conv );
3983 reader.SetDocument( doc );
3984
3985 if( !reader.Perform( TCollection_AsciiString( fname ), Message_ProgressRange() ) )
3986 return false;
3987
3988 return true;
3989#else
3990 return false;
3991#endif
3992}
3993
3994
3995TDF_Label STEP_PCB_MODEL::transferModel( Handle( TDocStd_Document ) & source,
3996 Handle( TDocStd_Document ) & dest, const VECTOR3D& aScale )
3997{
3998 Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( source->Main() );
3999
4000 NCollection_Sequence<TDF_Label> frshapes;
4001 s_assy->GetFreeShapes( frshapes );
4002
4003 Handle( XCAFDoc_ShapeTool ) d_assy = XCAFDoc_DocumentTool::ShapeTool( dest->Main() );
4004
4005 // Create a new top-level assembly in the destination and clone the source's free shapes
4006 // into it with XCAFDoc_Editor::Extract. Extract rebuilds the XDE label tree by walking
4007 // the component reference graph, so it preserves colors, names and sub-assembly structure
4008 // even when the source root does not directly own the part labels (as is the case with
4009 // default KiCad 3D models produced by CadQuery). It is also immune to the
4010 // "not self-contained" restriction of TDocStd_XLinkTool::Copy, so Fusion 360 STEP files
4011 // with linked components work as well.
4012 TDF_Label d_targetLabel = d_assy->NewShape();
4013
4014 if( !XCAFDoc_Editor::Extract( frshapes, d_targetLabel, false ) )
4015 {
4016 m_reporter->Report( wxT( "Failed to transfer model." ), RPT_SEVERITY_ERROR );
4017 return TDF_Label();
4018 }
4019
4020 if( aScale.x != 1.0 || aScale.y != 1.0 || aScale.z != 1.0 )
4021 rescaleShapes( d_targetLabel, gp_XYZ( aScale.x, aScale.y, aScale.z ) );
4022
4023 return d_targetLabel;
4024}
4025
4026
4027bool STEP_PCB_MODEL::performMeshing( Handle( XCAFDoc_ShapeTool ) & aShapeTool )
4028{
4029 NCollection_Sequence<TDF_Label> freeShapes;
4030 aShapeTool->GetFreeShapes( freeShapes );
4031
4032 m_reporter->Report( wxT( "Meshing model" ), RPT_SEVERITY_DEBUG );
4033
4034 // GLTF is a mesh format, we have to trigger opencascade to mesh the shapes we composited into the asesmbly
4035 // To mesh models, lets just grab the free shape root and execute on them
4036 for( int i = 1; i <= freeShapes.Length(); ++i )
4037 {
4038 TDF_Label label = freeShapes.Value( i );
4039 TopoDS_Shape shape;
4040 aShapeTool->GetShape( label, shape );
4041
4042 // These deflection values basically affect the accuracy of the mesh generated, a tighter
4043 // deflection will result in larger meshes
4044 // We could make this a tunable parameter, but for now fix it
4045 const double linearDeflection = 0.14;
4046 const double angularDeflection = DEG2RAD( 30.0 );
4047 BRepMesh_IncrementalMesh mesh( shape, linearDeflection, false, angularDeflection,
4048 true );
4049 }
4050
4051 return true;
4052}
4053
4054
4055bool STEP_PCB_MODEL::WriteGLTF( const wxString& aFileName )
4056{
4057 /*if( !isBoardOutlineValid() )
4058 {
4059 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
4060 aFileName ),
4061 RPT_SEVERITY_ERROR );
4062 return false;
4063 }*/
4064
4066
4067 performMeshing( m_assy );
4068
4069 wxFileName fn( aFileName );
4070
4071 const char* tmpGltfname = "$tempfile$.glb";
4072 RWGltf_CafWriter cafWriter( tmpGltfname, true );
4073
4074 cafWriter.SetTransformationFormat( RWGltf_WriterTrsfFormat_Compact );
4075 cafWriter.ChangeCoordinateSystemConverter().SetInputLengthUnit( 0.001 );
4076 cafWriter.ChangeCoordinateSystemConverter().SetInputCoordinateSystem(
4077 RWMesh_CoordinateSystem_Zup );
4078#if OCC_VERSION_HEX >= 0x070700
4079 cafWriter.SetParallel( true );
4080#endif
4081 TColStd_IndexedDataMapOfStringString metadata;
4082
4083 metadata.Add( TCollection_AsciiString( "pcb_name" ),
4084 TCollection_ExtendedString( fn.GetName().wc_str() ) );
4085 metadata.Add( TCollection_AsciiString( "source_pcb_file" ),
4086 TCollection_ExtendedString( fn.GetFullName().wc_str() ) );
4087 metadata.Add( TCollection_AsciiString( "generator" ),
4088 TCollection_AsciiString( wxString::Format( wxS( "KiCad %s" ), GetSemanticVersion() ).ToAscii() ) );
4089 metadata.Add( TCollection_AsciiString( "generated_at" ),
4090 TCollection_AsciiString( GetISO8601CurrentDateTime().ToAscii() ) );
4091
4092 bool success = true;
4093
4094 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
4095 wxString currCWD = wxGetCwd();
4096 wxString workCWD = fn.GetPath();
4097
4098 if( !workCWD.IsEmpty() )
4099 wxSetWorkingDirectory( workCWD );
4100
4101 success = cafWriter.Perform( m_doc, metadata, Message_ProgressRange() );
4102
4103 if( success )
4104 {
4105 // OCCT 7.9+ can produce LINES primitives with odd index counts for degenerate
4106 // BSpline edges, violating the glTF spec and causing Blender import failures. A
4107 // failure here leaves the original writer output intact, so warn and keep going.
4108 if( !FixGlbLinesPrimitives( wxString( tmpGltfname ) ) )
4109 {
4110 m_reporter->Report( _( "Could not post-process GLB line primitives; the exported "
4111 "model may not import in strict glTF viewers." ),
4113 }
4114
4115 // Preserve the permissions of the current file
4116 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpGltfname );
4117
4118 if( !wxRenameFile( tmpGltfname, fn.GetFullName(), true ) )
4119 {
4120 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'." ),
4121 tmpGltfname,
4122 fn.GetFullName() ),
4124 success = false;
4125 }
4126 }
4127
4128 wxSetWorkingDirectory( currCWD );
4129
4130 return success;
4131}
4132
4133
4134bool STEP_PCB_MODEL::WritePLY( const wxString& aFileName )
4135{
4136#if OCC_VERSION_HEX < 0x070700
4137 m_reporter->Report( wxT( "PLY export is not supported before OCCT 7.7.0" ), RPT_SEVERITY_ERROR );
4138 return false;
4139#else
4140
4141 if( !isBoardOutlineValid() )
4142 {
4143 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
4144 aFileName ),
4146 return false;
4147 }
4148
4150
4151 performMeshing( m_assy );
4152
4153 wxFileName fn( aFileName );
4154
4155 const char* tmpFname = "$tempfile$.ply";
4156 RWPly_CafWriter cafWriter( tmpFname );
4157
4158 cafWriter.SetFaceId( true ); // TODO: configurable SetPartId/SetFaceId
4159 cafWriter.ChangeCoordinateSystemConverter().SetInputLengthUnit( 0.001 );
4160 cafWriter.ChangeCoordinateSystemConverter().SetInputCoordinateSystem( RWMesh_CoordinateSystem_Zup );
4161
4162 TColStd_IndexedDataMapOfStringString metadata;
4163
4164 metadata.Add( TCollection_AsciiString( "pcb_name" ),
4165 TCollection_ExtendedString( fn.GetName().wc_str() ) );
4166 metadata.Add( TCollection_AsciiString( "source_pcb_file" ),
4167 TCollection_ExtendedString( fn.GetFullName().wc_str() ) );
4168 metadata.Add( TCollection_AsciiString( "generator" ),
4169 TCollection_AsciiString( wxString::Format( wxS( "KiCad %s" ),
4170 GetSemanticVersion() ).ToAscii() ) );
4171 metadata.Add( TCollection_AsciiString( "generated_at" ),
4172 TCollection_AsciiString( GetISO8601CurrentDateTime().ToAscii() ) );
4173
4174 bool success = true;
4175
4176 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
4177 wxString currCWD = wxGetCwd();
4178 wxString workCWD = fn.GetPath();
4179
4180 if( !workCWD.IsEmpty() )
4181 wxSetWorkingDirectory( workCWD );
4182
4183 success = cafWriter.Perform( m_doc, metadata, Message_ProgressRange() );
4184
4185 if( success )
4186 {
4187 // Preserve the permissions of the current file
4188 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpFname );
4189
4190 if( !wxRenameFile( tmpFname, fn.GetFullName(), true ) )
4191 {
4192 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'." ),
4193 tmpFname,
4194 fn.GetFullName() ),
4196 success = false;
4197 }
4198 }
4199
4200 wxSetWorkingDirectory( currCWD );
4201
4202 return success;
4203#endif
4204}
4205
4206
4207bool STEP_PCB_MODEL::WriteSTL( const wxString& aFileName )
4208{
4209 if( !isBoardOutlineValid() )
4210 {
4211 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
4212 aFileName ),
4214 return false;
4215 }
4216
4218
4219 performMeshing( m_assy );
4220
4221 wxFileName fn( aFileName );
4222
4223 const char* tmpFname = "$tempfile$.stl";
4224
4225 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
4226 wxString currCWD = wxGetCwd();
4227 wxString workCWD = fn.GetPath();
4228
4229 if( !workCWD.IsEmpty() )
4230 wxSetWorkingDirectory( workCWD );
4231
4232 bool success = StlAPI_Writer().Write( getOneShape( m_assy ), tmpFname );
4233
4234 if( success )
4235 {
4236 // Preserve the permissions of the current file
4237 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpFname );
4238
4239 if( !wxRenameFile( tmpFname, fn.GetFullName(), true ) )
4240 {
4241 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'." ),
4242 tmpFname,
4243 fn.GetFullName() ),
4245 success = false;
4246 }
4247 }
4248
4249 wxSetWorkingDirectory( currCWD );
4250
4251 return success;
4252}
4253
4254
4255
4256bool STEP_PCB_MODEL::WriteU3D( const wxString& aFileName )
4257{
4258 if( !isBoardOutlineValid() )
4259 {
4260 m_reporter->Report(
4261 wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'.\n" ), aFileName ),
4263 return false;
4264 }
4265
4267
4268 performMeshing( m_assy );
4269
4270 wxFileName fn( aFileName );
4271
4272 const char* tmpFname = "$tempfile$.u3d";
4273
4274 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
4275 wxString currCWD = wxGetCwd();
4276 wxString workCWD = fn.GetPath();
4277
4278 if( !workCWD.IsEmpty() )
4279 wxSetWorkingDirectory( workCWD );
4280
4281 U3D::WRITER writer( tmpFname );
4282 bool success = writer.Perform( m_doc );
4283 if( success )
4284 {
4285 // Preserve the permissions of the current file
4286 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpFname );
4287
4288 if( !wxRenameFile( tmpFname, fn.GetFullName(), true ) )
4289 {
4290 m_reporter->Report( wxString::Format( wxT( "Cannot rename temporary file '%s' to '%s'.\n" ), tmpFname,
4291 fn.GetFullName() ),
4293 success = false;
4294 }
4295 }
4296
4297 wxSetWorkingDirectory( currCWD );
4298
4299 return success;
4300}
4301
4302
4303bool STEP_PCB_MODEL::WritePDF( const wxString& aFileName )
4304{
4305 if( !isBoardOutlineValid() )
4306 {
4307 m_reporter->Report(
4308 wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'.\n" ), aFileName ),
4310 return false;
4311 }
4312
4314
4315 performMeshing( m_assy );
4316
4317 wxFileName fn( aFileName );
4318
4319 wxFileName u3dTmpfn = wxFileName::CreateTempFileName( "" );
4320 wxFileName pdfTmpfn = wxFileName::CreateTempFileName( "" );
4321
4322 U3D::WRITER writer( u3dTmpfn.GetFullPath().ToStdString() );
4323 bool success = writer.Perform( m_doc );
4324
4325 // PDF test
4326 std::unique_ptr<PDF_PLOTTER> plotter = std::make_unique<PDF_PLOTTER>();
4327
4328 plotter->SetColorMode( true );
4329 plotter->Set3DExport( true );
4330 plotter->SetCreator( wxT( "Mark's awesome 3d exporter" ) );
4331 KIGFX::PCB_RENDER_SETTINGS renderSettings;
4332 plotter->SetRenderSettings( &renderSettings );
4333
4334 if( !plotter->OpenFile( pdfTmpfn.GetFullPath() ) )
4335 {
4336 m_reporter->Report( wxString::Format( wxT( "Cannot open temporary file '%s'.\n" ), pdfTmpfn.GetFullPath() ),
4338 success = false;
4339 }
4340 else
4341 {
4342 plotter->StartPlot( "1", "3D Model" );
4343 double fov_degrees = 16.5f;
4344
4345 // kind of an arbitrary distance determination
4346 float distance = sqrt( writer.GetMeshBoundingBox().SquareExtent() ) * 3;
4347
4348 std::vector<PDF_3D_VIEW> views;
4349
4350 VECTOR3D camTarget = writer.GetCenter();
4351
4352
4353 std::vector<float> c2wMatrix =
4354 PDF_PLOTTER::CreateC2WMatrixFromAngles( camTarget, distance, 180.0f, -75.0f, 25.0f );
4355
4356 views.emplace_back( PDF_3D_VIEW{
4357 .m_name = "Default",
4358 .m_cameraMatrix = c2wMatrix,
4359 .m_cameraCenter = (float) distance,
4360 .m_fov = (float) fov_degrees,
4361 } );
4362
4363
4364
4365 c2wMatrix = PDF_PLOTTER::CreateC2WMatrixFromAngles( camTarget, distance, 180.0, 0.0f, 0.0f );
4366
4367 views.emplace_back( PDF_3D_VIEW{
4368 .m_name = "Top",
4369 .m_cameraMatrix = c2wMatrix,
4370 .m_cameraCenter = (float) distance,
4371 .m_fov = (float) fov_degrees,
4372 } );
4373
4374
4375
4376 c2wMatrix = PDF_PLOTTER::CreateC2WMatrixFromAngles( camTarget, distance, 0.0, 0.0f, 0.0f );
4377
4378 views.emplace_back( PDF_3D_VIEW{
4379 .m_name = "Bottom",
4380 .m_cameraMatrix = c2wMatrix,
4381 .m_cameraCenter = (float) distance,
4382 .m_fov = (float) fov_degrees,
4383 } );
4384
4385
4386
4387 c2wMatrix = PDF_PLOTTER::CreateC2WMatrixFromAngles( camTarget, distance, 90.0f, -90.0f, 90.0f );
4388
4389 views.emplace_back( PDF_3D_VIEW{
4390 .m_name = "Front",
4391 .m_cameraMatrix = c2wMatrix,
4392 .m_cameraCenter = (float) distance,
4393 .m_fov = (float) fov_degrees,
4394 } );
4395
4396 plotter->Plot3DModel( u3dTmpfn.GetFullPath(), views );
4397 plotter->EndPlot();
4398 }
4399
4400 if( success )
4401 {
4402 // Preserve the permissions of the current file
4403 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), pdfTmpfn.GetFullPath() );
4404
4405 if( !wxRenameFile( pdfTmpfn.GetFullPath(), fn.GetFullPath(), true ) )
4406 {
4407 m_reporter->Report( wxString::Format( wxT( "Cannot rename temporary file '%s' to '%s'.\n" ),
4408 pdfTmpfn.GetFullPath(), fn.GetFullPath() ),
4410 success = false;
4411 }
4412 }
4413
4414 wxRemoveFile( u3dTmpfn.GetFullPath() );
4415
4416 return success;
4417}
void ApplyExtrusionTransform(SHAPE_POLY_SET &aOutline, const EXTRUDED_3D_BODY *aBody, const VECTOR2I &aFpPos)
Apply 2D extrusion transforms (rotation, scale, offset) to an outline.
bool GetExtrusionPinOutline(const FOOTPRINT *aFootprint, SHAPE_POLY_SET &aPinPoly)
Get the pin outline polygons for extruded THT pin rendering.
int index
const char * name
@ ERROR_OUTSIDE
@ ERROR_INSIDE
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
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:918
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:126
int GetX() const
Definition board_item.h:120
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:224
constexpr coord_type GetRight() const
Definition box2.h:213
constexpr coord_type GetTop() const
Definition box2.h:225
constexpr coord_type GetBottom() const
Definition box2.h:218
double AsDegrees() const
Definition eda_angle.h:116
const EXTRUDED_3D_BODY * GetExtrudedBody() const
Definition footprint.h:399
wxString GetReferenceAsString() const
Definition footprint.h:866
VECTOR2I GetPosition() const override
Definition footprint.h:406
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:101
double r
Red component.
Definition color4d.h:389
double g
Green component.
Definition color4d.h:390
COLOR4D & Darken(double aFactor)
Makes the color darker by a given factor.
Definition color4d.h:223
double a
Alpha component.
Definition color4d.h:392
double b
Blue component.
Definition color4d.h:391
PCB specific render settings.
Definition pcb_painter.h:80
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:558
LSET GetLayerSet() const override
Return a std::bitset of all layers on which the item physically resides.
Definition pad.h:552
bool FlashLayer(int aLayer, bool aOnlyCheckIfPermitted=false) const
Check to see whether the pad should be flashed on the specific layer.
Definition pad.cpp:650
bool IsOnLayer(PCB_LAYER_ID aLayer) const override
Test to see if this object is on the given layer.
Definition pad.h:912
PAD_ATTRIB GetAttribute() const
Definition pad.h:555
const wxString & GetNumber() const
Definition pad.h:143
VECTOR2I GetDrillSize() const
Definition pad.h:315
void TransformShapeToPolygon(SHAPE_POLY_SET &aBuffer, PCB_LAYER_ID aLayer, int aClearance, int aMaxError, ERROR_LOC aErrorLoc=ERROR_INSIDE, bool ignoreLineWidth=false) const override
Convert the pad shape to a closed polygon.
Definition pad.cpp:2945
std::shared_ptr< SHAPE_SEGMENT > GetEffectiveHoleShape() const override
Return a SHAPE_SEGMENT object representing the pad's hole.
Definition pad.cpp:1312
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:72
virtual REPORTER & Report(const wxString &aText, SEVERITY aSeverity=RPT_SEVERITY_UNDEFINED)
Report a string with a given severity.
Definition reporter.h:101
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
wxString m_pcbName
Name of the PCB, which will most likely be the file name of the path.
std::vector< TopoDS_Shape > m_boardCutouts
bool AddPolygonShapes(const SHAPE_POLY_SET *aPolyShapes, PCB_LAYER_ID aLayer, const VECTOR2D &aOrigin, const wxString &aNetname)
bool CreatePCB(SHAPE_POLY_SET &aOutline, const VECTOR2D &aOrigin, bool aPushBoardBody)
void getBoardBodyZPlacement(double &aZPos, double &aThickness)
TDF_Label transferModel(Handle(TDocStd_Document)&source, Handle(TDocStd_Document) &dest, const VECTOR3D &aScale)
TDF_Label m_assy_label
bool getModelLocation(bool aBottom, const VECTOR2D &aPosition, double aRotation, const VECTOR3D &aOffset, const VECTOR3D &aOrientation, TopLoc_Location &aLocation)
void SetFuseShapes(bool aValue)
bool WriteXAO(const wxString &aFileName)
bool WriteGLTF(const wxString &aFileName)
Write the assembly in binary GLTF Format.
REPORTER * m_reporter
std::vector< TDF_Label > m_pcb_labels
STEP_PCB_MODEL(const wxString &aPcbName, REPORTER *aReporter)
std::vector< EXTRUDED_BODY_ENTRY > m_extruded_bodies
bool AddBackdrill(const SHAPE_SEGMENT &aShape, PCB_LAYER_ID aLayerStart, PCB_LAYER_ID aLayerEnd, const VECTOR2D &aOrigin)
Add a backdrill hole shape to remove board material and copper plating.
void getLayerZPlacement(PCB_LAYER_ID aLayer, double &aZPos, double &aThickness)
std::vector< TopoDS_Shape > m_board_outlines
void SetSimplifyShapes(bool aValue)
bool WriteU3D(const wxString &aFileName)
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
bool AddExtrudedPins(const FOOTPRINT *aFootprint, bool aBottom, double aStandoff, const VECTOR2D &aOrigin)
Add metallic pin extrusions for through-hole pads.
Handle(XCAFApp_Application) m_app
bool WritePDF(const wxString &aFileName)
virtual ~STEP_PCB_MODEL()
std::vector< TopoDS_Shape > m_board_front_mask
double m_copperColor[3]
bool WriteSTL(const wxString &aFileName)
void SetStackup(const BOARD_STACKUP &aStackup)
void SetNetFilter(const wxString &aFilter)
double m_padColor[3]
std::map< wxString, std::vector< TopoDS_Shape > > m_board_copper
std::map< PCB_LAYER_ID, int > GetCopperLayerKnockouts(int aDiameter, int aDepth, int aAngle, bool aFrontSide)
Get the knockout diameters for copper layers that a counterbore or countersink crosses.
bool MakeShapes(std::vector< TopoDS_Shape > &aShapes, const SHAPE_POLY_SET &aPolySet, bool aConvertToArcs, double aThickness, double aZposition, const VECTOR2D &aOrigin)
Convert a SHAPE_POLY_SET to TopoDS_Shape's (polygonal vertical prisms, or flat faces)
bool AddBarrel(const SHAPE_SEGMENT &aShape, PCB_LAYER_ID aLayerTop, PCB_LAYER_ID aLayerBot, bool aVia, const VECTOR2D &aOrigin, const wxString &aNetname)
bool readIGES(Handle(TDocStd_Document) &aDoc, const char *aFname)
bool AddHole(const SHAPE_SEGMENT &aShape, int aPlatingThickness, PCB_LAYER_ID aLayerTop, PCB_LAYER_ID aLayerBot, bool aVia, const VECTOR2D &aOrigin, bool aCutCopper, bool aCutBody)
std::vector< TopoDS_Shape > m_copperCutouts
bool CompressSTEP(wxString &inputFile, wxString &outputFile)
std::vector< TopoDS_Shape > m_board_back_silk
void getCopperLayerZPlacement(PCB_LAYER_ID aLayer, double &aZPos, double &aThickness)
bool AddComponent(const wxString &aBaseName, const wxString &aFileName, const std::vector< wxString > &aAltFilenames, const wxString &aRefDes, bool aBottom, VECTOR2D aPosition, double aRotation, VECTOR3D aOffset, VECTOR3D aOrientation, VECTOR3D aScale, bool aSubstituteModels=true)
bool AddExtrudedBody(const SHAPE_POLY_SET &aOutline, bool aBottom, double aStandoff, double aHeight, const VECTOR2D &aOrigin, uint32_t aColor, EXTRUSION_MATERIAL aMaterial, const wxString &aRefDes)
Add an extruded 3D body from a 2D outline polygon.
std::map< wxString, std::vector< std::pair< gp_Pnt, TopoDS_Shape > > > m_pad_points
bool AddPadShape(const PAD *aPad, const VECTOR2D &aOrigin, bool aVia, SHAPE_POLY_SET *aClipPolygon=nullptr)
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:279
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:31
static constexpr EDA_ANGLE ANGLE_360
Definition eda_angle.h:417
EXTRUSION_MATERIAL
Definition footprint.h:93
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.
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:786
bool IsBackLayer(PCB_LAYER_ID aLayerId)
Layer classification: check if it's a back layer.
Definition layer_ids.h:809
bool IsCopperLayer(int aLayerId)
Test whether a layer is a copper layer.
Definition layer_ids.h:683
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:400
@ PTH
Plated through hole pad.
Definition padstack.h:98
@ CASTELLATED
a pad with a castellated through hole
Definition padstack.h:121
#define _HKI(x)
Definition page_info.cpp: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 ...
std::pair< std::string, TDF_Label > MODEL_DATUM
#define CLOSE_STREAM(var)
#define OPEN_ISTREAM(var, name)
wxString UnescapeString(const wxString &aSource)
wxString GetISO8601CurrentDateTime()
KIBIS top(path, &reporter)
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:225
double DEG2RAD(double deg)
Definition trigo.h:162
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683
VECTOR2< double > VECTOR2D
Definition vector2d.h:682
VECTOR3< double > VECTOR3D
Definition vector3.h:230