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