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