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