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