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 std::vector<Bnd_Box>& holeBoxes )
2509 {
2510 // We need to encompass every location we'll need to test in the global bbox,
2511 // otherwise Bnd_BoundSortBox doesn't work near the boundaries.
2512 Bnd_Box brdWithHolesBndBox = brdBndBox;
2513
2514 Handle( Bnd_HArray1OfBox ) holeBoxSet = new Bnd_HArray1OfBox( 0, input.size() - 1 );
2515 holeBoxes.resize( input.size() );
2516
2517 for( size_t i = 0; i < input.size(); i++ )
2518 {
2519 Bnd_Box bbox;
2520 BRepBndLib::Add( input[i], bbox );
2521 brdWithHolesBndBox.Add( bbox );
2522 ( *holeBoxSet )[i] = bbox;
2523 holeBoxes[i] = bbox;
2524 }
2525
2526 bsbHoles.Initialize( brdWithHolesBndBox, holeBoxSet );
2527 };
2528
2529 auto subtractShapesMap =
2530 [&tp, this]( const wxString& aWhat, std::map<wxString, std::vector<TopoDS_Shape>>& aShapesMap,
2531 std::vector<TopoDS_Shape>& aHolesList, Bnd_BoundSortBox& aBSBHoles,
2532 const std::vector<Bnd_Box>& aHoleBoxes )
2533 {
2534 m_reporter->Report( wxString::Format( _( "Subtracting holes for %s" ), aWhat ),
2536
2537 for( auto& [netname, vec] : aShapesMap )
2538 {
2539 std::mutex mutex;
2540
2541 auto subtractLoopFn = [&]( const int shapeId )
2542 {
2543 TopoDS_Shape& shape = vec[shapeId];
2544
2545 Bnd_Box shapeBbox;
2546 BRepBndLib::Add( shape, shapeBbox );
2547
2548 TopTools_ListOfShape holelist;
2549
2550 {
2551 std::unique_lock lock( mutex );
2552
2553 const TColStd_ListOfInteger& indices = aBSBHoles.Compare( shapeBbox );
2554
2555 for( const Standard_Integer& index : indices )
2556 holelist.Append( aHolesList[index] );
2557
2558 // Workaround for OCCT bug (https://github.com/Open-Cascade-SAS/OCCT/issues/506)
2559 // Bnd_BoundSortBox::Compare can fail to detect intersections in certain edge
2560 // cases (e.g., single item). Fall back to direct bounding box intersection
2561 // checks when Compare returns empty but intersections may exist.
2562 if( holelist.IsEmpty() )
2563 {
2564 for( size_t i = 0; i < aHoleBoxes.size(); i++ )
2565 {
2566 if( !shapeBbox.IsOut( aHoleBoxes[i] ) )
2567 holelist.Append( aHolesList[i] );
2568 }
2569 }
2570 }
2571
2572 if( holelist.IsEmpty() )
2573 return; // nothing to cut for this shape
2574
2575 TopTools_ListOfShape cutArgs;
2576 cutArgs.Append( shape );
2577
2578 BRepAlgoAPI_Cut cut;
2579
2580 cut.SetRunParallel( true );
2581 cut.SetToFillHistory( false );
2582
2583 cut.SetArguments( cutArgs );
2584 cut.SetTools( holelist );
2585 cut.Build();
2586
2587 if( cut.HasErrors() || cut.HasWarnings() )
2588 {
2589 m_reporter->Report( wxString::Format( _( "** Got problems while cutting "
2590 "%s net '%s' **" ),
2591 aWhat,
2592 UnescapeString( netname ) ),
2594 shapeBbox.Dump();
2595
2596 if( cut.HasErrors() )
2597 {
2598 wxString msg = _( "Errors:\n" );
2599 wxStringOutputStream os_stream( &msg );
2600 wxStdOutputStream out( os_stream );
2601
2602 cut.DumpErrors( out );
2603 m_reporter->Report( msg, RPT_SEVERITY_WARNING);
2604 }
2605
2606 if( cut.HasWarnings() )
2607 {
2608 wxString msg = _( "Warnings:\n" );
2609 wxStringOutputStream os_stream( &msg );
2610 wxStdOutputStream out( os_stream );
2611
2612 cut.DumpWarnings( out );
2613 m_reporter->Report( msg, RPT_SEVERITY_WARNING );
2614 }
2615 }
2616
2617 shape = cut.Shape();
2618 };
2619
2620 tp.submit_loop( 0, vec.size(), subtractLoopFn ).wait();
2621 }
2622 };
2623
2624 auto subtractShapes =
2625 [subtractShapesMap]( const wxString& aWhat, std::vector<TopoDS_Shape>& aShapesList,
2626 std::vector<TopoDS_Shape>& aHolesList, Bnd_BoundSortBox& aBSBHoles,
2627 const std::vector<Bnd_Box>& aHoleBoxes )
2628 {
2629 std::map<wxString, std::vector<TopoDS_Shape>> aShapesMap{ { wxEmptyString, aShapesList } };
2630
2631 subtractShapesMap( aWhat, aShapesMap, aHolesList, aBSBHoles, aHoleBoxes );
2632 aShapesList = aShapesMap[wxEmptyString];
2633 };
2634
2635
2636 if( m_boardCutouts.size() )
2637 {
2638 Bnd_BoundSortBox bsbHoles;
2639 std::vector<Bnd_Box> holeBoxes;
2640 buildBSB( m_boardCutouts, bsbHoles, holeBoxes );
2641
2642 subtractShapes( _( "shapes" ), m_board_outlines, m_boardCutouts, bsbHoles, holeBoxes );
2643 }
2644
2645 if( m_copperCutouts.size() )
2646 {
2647 Bnd_BoundSortBox bsbHoles;
2648 std::vector<Bnd_Box> holeBoxes;
2649 buildBSB( m_copperCutouts, bsbHoles, holeBoxes );
2650
2651 subtractShapesMap( _( "pads" ), m_board_copper_pads, m_copperCutouts, bsbHoles, holeBoxes );
2652 subtractShapesMap( _( "vias" ), m_board_copper_vias, m_copperCutouts, bsbHoles, holeBoxes );
2653 }
2654
2655 if( m_fuseShapes )
2656 {
2657 std::map<wxString, TopTools_ListOfShape> shapesToFuseMap;
2658
2659 auto addShapes = [&shapesToFuseMap]( const wxString& aNetname,
2660 const std::vector<TopoDS_Shape>& aShapes )
2661 {
2662 for( const TopoDS_Shape& shape : aShapes )
2663 shapesToFuseMap[aNetname].Append( shape );
2664 };
2665
2666 for( const auto& [netname, shapes] : m_board_copper )
2667 addShapes( netname, shapes );
2668
2669 for( const auto& [netname, shapes] : m_board_copper_pads )
2670 addShapes( netname, shapes );
2671
2672 for( const auto& [netname, shapes] : m_board_copper_vias )
2673 addShapes( netname, shapes );
2674
2675 m_reporter->Report( wxT( "Fusing shapes" ), RPT_SEVERITY_DEBUG );
2676
2677 // Do fusing in parallel
2678 std::mutex mutex;
2679
2680 auto fuseLoopFn = [&]( const wxString& aNetname )
2681 {
2682 auto& toFuse = shapesToFuseMap[aNetname];
2683 TopoDS_Shape fusedShape = fuseShapesOrCompound( toFuse, m_reporter );
2684
2685 if( !fusedShape.IsNull() )
2686 {
2687 std::unique_lock lock( mutex );
2688
2689 m_board_copper_fused[aNetname].emplace_back( fusedShape );
2690
2691 m_board_copper[aNetname].clear();
2692 m_board_copper_pads[aNetname].clear();
2693 m_board_copper_vias[aNetname].clear();
2694 }
2695 };
2696
2697 BS::multi_future<void> mf;
2698
2699 for( const auto& [netname, _] : shapesToFuseMap )
2700 mf.push_back( tp.submit_task( [&, netname]() { fuseLoopFn( netname ); } ) );
2701
2702 mf.wait();
2703 }
2704
2705 // push the board to the data structure
2706 m_reporter->Report( wxT( "Generate board full shape." ), RPT_SEVERITY_DEBUG );
2707
2708 // AddComponent adds a label that has a reference (not a parent/child relation) to the real
2709 // label. We need to extract that real label to name it for the STEP output cleanly
2710 // Why are we trying to name the bare board? Because CAD tools like SolidWorks do fun things
2711 // like "deduplicate" imported STEPs by swapping STEP assembly components with already
2712 // identically named assemblies. So we want to avoid having the PCB be generally defaulted
2713 // to "Component" or "Assembly".
2714
2715 // aCompoundNets will place all geometry within a net into one compound.
2716 // aCompoundAll will place all geometry into one compound.
2717 auto pushToAssemblyMap =
2718 [&]( const std::map<wxString, std::vector<TopoDS_Shape>>& aShapesMap,
2719 const TDF_Label& aVisMatLabel, const wxString& aShapeName, bool aCompoundNets,
2720 bool aCompoundAll, const wxString& aNiceName )
2721 {
2722 std::map<wxString, std::vector<TopoDS_Shape>> shapesMap;
2723
2724 if( aCompoundAll )
2725 {
2726 std::vector<TopoDS_Shape> allShapes;
2727
2728 for( const auto& [netname, shapesList] : aShapesMap )
2729 allShapes.insert( allShapes.end(), shapesList.begin(), shapesList.end() );
2730
2731 if( !allShapes.empty() )
2732 shapesMap[wxEmptyString].emplace_back( makeCompound( allShapes ) );
2733 }
2734 else
2735 {
2736 shapesMap = aShapesMap;
2737 }
2738
2739 for( const auto& [netname, shapesList] : shapesMap )
2740 {
2741 std::vector<TopoDS_Shape> newList;
2742
2743 if( aCompoundNets )
2744 newList.emplace_back( makeCompound( shapesList ) );
2745 else
2746 newList = shapesList;
2747
2748 int i = 1;
2749
2750 for( TopoDS_Shape& shape : newList )
2751 {
2752 Handle( TDataStd_TreeNode ) node;
2753
2754 // Dont expand the component or else coloring it gets hard
2755 TDF_Label lbl = m_assy->AddComponent( m_assy_label, shape, false );
2756 KICAD3D_INFO::Set( lbl, KICAD3D_MODEL_TYPE::BOARD, aNiceName.ToStdString() );
2757 m_pcb_labels.push_back( lbl );
2758
2759 if( m_pcb_labels.back().IsNull() )
2760 return;
2761
2762 lbl.FindAttribute( XCAFDoc::ShapeRefGUID(), node );
2763 TDF_Label shpLbl = node->Father()->Label();
2764
2765 if( !shpLbl.IsNull() )
2766 {
2767 if( visMatTool && !aVisMatLabel.IsNull() )
2768 visMatTool->SetShapeMaterial( shpLbl, aVisMatLabel );
2769
2770 wxString shapeName;
2771
2772 shapeName << m_pcbName;
2773 shapeName << '_';
2774 shapeName << aShapeName;
2775
2776 if( !netname.empty() )
2777 {
2778 shapeName << '_';
2779 shapeName << netname;
2780 }
2781
2782 if( newList.size() > 1 )
2783 {
2784 shapeName << '_';
2785 shapeName << i;
2786 }
2787
2788 TCollection_ExtendedString partname( shapeName.ToUTF8().data() );
2789 TDataStd_Name::Set( shpLbl, partname );
2790 }
2791
2792 i++;
2793 }
2794 }
2795 };
2796
2797 auto pushToAssembly =
2798 [&]( const std::vector<TopoDS_Shape>& aShapesList, const TDF_Label& aVisMatLabel,
2799 const wxString& aShapeName, bool aCompound, const wxString& aNiceName )
2800 {
2801 const std::map<wxString, std::vector<TopoDS_Shape>> shapesMap{ { wxEmptyString, aShapesList } };
2802
2803 pushToAssemblyMap( shapesMap, aVisMatLabel, aShapeName, aCompound, aCompound, aNiceName );
2804 };
2805
2806 auto makeMaterial =
2807 [&]( const TCollection_AsciiString& aName, const Quantity_ColorRGBA& aBaseColor,
2808 double aMetallic, double aRoughness ) -> TDF_Label
2809 {
2810 Handle( XCAFDoc_VisMaterial ) vismat = new XCAFDoc_VisMaterial;
2811 XCAFDoc_VisMaterialPBR pbr;
2812 pbr.BaseColor = aBaseColor;
2813 pbr.Metallic = aMetallic;
2814 pbr.Roughness = aRoughness;
2815 vismat->SetPbrMaterial( pbr );
2816 return visMatTool->AddMaterial( vismat, aName );
2817 };
2818
2819 // Init colors for the board items
2820 Quantity_ColorRGBA copper_color( m_copperColor[0], m_copperColor[1], m_copperColor[2], 1.0 );
2821 Quantity_ColorRGBA pad_color( m_padColor[0], m_padColor[1], m_padColor[2], 1.0 );
2822
2823 Quantity_ColorRGBA board_color( 0.42f, 0.45f, 0.29f, 0.98f );
2824 Quantity_ColorRGBA front_silk_color( 1.0f, 1.0f, 1.0f, 0.9f );
2825 Quantity_ColorRGBA back_silk_color = front_silk_color;
2826 Quantity_ColorRGBA front_mask_color( 0.08f, 0.2f, 0.14f, 0.83f );
2827 Quantity_ColorRGBA back_mask_color = front_mask_color;
2828
2829 // Get colors from stackup
2830 for( const BOARD_STACKUP_ITEM* item : m_stackup.GetList() )
2831 {
2832 COLOR4D col;
2833
2834 if( !colorFromStackup( item->GetType(), item->GetColor(), col ) )
2835 continue;
2836
2837 if( item->GetBrdLayerId() == F_Mask || item->GetBrdLayerId() == B_Mask )
2838 {
2839 col.Darken( 0.2 );
2840
2841 if( item->GetBrdLayerId() == F_Mask )
2842 front_mask_color.SetValues( col.r, col.g, col.b, col.a );
2843 else
2844 back_mask_color.SetValues( col.r, col.g, col.b, col.a );
2845 }
2846
2847 if( item->GetBrdLayerId() == F_SilkS )
2848 front_silk_color.SetValues( col.r, col.g, col.b, col.a );
2849 else if( item->GetBrdLayerId() == B_SilkS )
2850 back_silk_color.SetValues( col.r, col.g, col.b, col.a );
2851
2852 if( item->GetType() == BS_ITEM_TYPE_DIELECTRIC && item->GetTypeName() == KEY_CORE )
2853 board_color.SetValues( col.r, col.g, col.b, col.a );
2854 }
2855
2856 // Paint board body in soldermask colors if soldermask is not exported as a layer
2857 if( !m_enabledLayers.Contains( F_Mask ) && !m_enabledLayers.Contains( B_Mask ) )
2858 {
2859 board_color = front_mask_color;
2860 board_color.SetAlpha( 1.0 );
2861 }
2862
2863 TDF_Label front_mask_mat = makeMaterial( "soldermask", front_mask_color, 0.0, 0.6 );
2864 TDF_Label back_mask_mat = makeMaterial( "soldermask", back_mask_color, 0.0, 0.6 );
2865 TDF_Label front_silk_mat = makeMaterial( "silkscreen", front_silk_color, 0.0, 0.9 );
2866 TDF_Label back_silk_mat = makeMaterial( "silkscreen", back_silk_color, 0.0, 0.9 );
2867 TDF_Label copper_mat = makeMaterial( "copper", copper_color, 1.0, 0.4 );
2868 TDF_Label pad_mat = makeMaterial( "pad", pad_color, 1.0, 0.4 );
2869 TDF_Label board_mat = makeMaterial( "board", board_color, 0.0, 0.8 );
2870
2871 pushToAssemblyMap( m_board_copper, copper_mat, "copper", true, true, "Copper" );
2872 pushToAssemblyMap( m_board_copper_pads, pad_mat, "pad", true, true, "Pads" );
2873 pushToAssemblyMap( m_board_copper_vias, copper_mat, "via", true, true, "Via" );
2874 pushToAssemblyMap( m_board_copper_fused, copper_mat, "copper", true, true, "Copper" );
2875 pushToAssembly( m_board_front_silk, front_silk_mat, "silkscreen", true, "Top Silkscreen" );
2876 pushToAssembly( m_board_back_silk, back_silk_mat, "silkscreen", true, "Bottom Silkscreen" );
2877 pushToAssembly( m_board_front_mask, front_mask_mat, "soldermask", true, "Top Soldermask" );
2878 pushToAssembly( m_board_back_mask, back_mask_mat, "soldermask", true, "Bottom Soldermask" );
2879
2880 if( aPushBoardBody )
2881 pushToAssembly( m_board_outlines, board_mat, "PCB", false, "Body" );
2882
2883#if( defined OCC_VERSION_HEX ) && ( OCC_VERSION_HEX > 0x070101 )
2884 m_assy->UpdateAssemblies();
2885#endif
2886
2887 return true;
2888}
2889
2890
2891#ifdef SUPPORTS_IGES
2892// write the assembly model in IGES format
2893bool STEP_PCB_MODEL::WriteIGES( const wxString& aFileName )
2894{
2895 if( !isBoardOutlineValid() )
2896 {
2897 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
2898 aFileName ),
2900 return false;
2901 }
2902
2904
2905 wxFileName fn( aFileName );
2906 IGESControl_Controller::Init();
2907 IGESCAFControl_Writer writer;
2908 writer.SetColorMode( Standard_True );
2909 writer.SetNameMode( Standard_True );
2910 IGESData_GlobalSection header = writer.Model()->GlobalSection();
2911 header.SetFileName( new TCollection_HAsciiString( fn.GetFullName().ToAscii() ) );
2912 header.SetSendName( new TCollection_HAsciiString( "KiCad electronic assembly" ) );
2913 header.SetAuthorName( new TCollection_HAsciiString( Interface_Static::CVal( "write.iges.header.author" ) ) );
2914 header.SetCompanyName( new TCollection_HAsciiString( Interface_Static::CVal( "write.iges.header.company" ) ) );
2915 writer.Model()->SetGlobalSection( header );
2916
2917 if( Standard_False == writer.Perform( m_doc, aFileName.c_str() ) )
2918 return false;
2919
2920 return true;
2921}
2922#endif
2923
2924bool STEP_PCB_MODEL::CompressSTEP( wxString& inputFile, wxString& outputFile )
2925{
2926 wxFileInputStream input( inputFile );
2927 wxFileOutputStream output( outputFile );
2928
2929 if( !input.IsOk() )
2930 {
2931 m_reporter->Report( wxString::Format( _( "Cannot create input stream '%s'.\n" ), inputFile ) );
2932 return false;
2933 }
2934
2935 if( !output.IsOk() )
2936 {
2937 m_reporter->Report( wxString::Format( _( "Cannot create output stream '%s'.\n" ), outputFile ) );
2938 return false;
2939 }
2940
2941 wxZlibOutputStream zlibStream( output, -1, wxZLIB_GZIP );
2942
2943 if( !zlibStream.IsOk() )
2944 {
2945 m_reporter->Report( _( "Impossible create compress stream" ) );
2946 return false;
2947 }
2948
2949 input.Read( zlibStream );
2950
2951 if( input.LastRead() == 0 || zlibStream.LastWrite() == 0 )
2952 {
2953 m_reporter->Report( _( "Compress read or write error" ) );
2954 return false;
2955 }
2956
2957 zlibStream.Close();
2958 output.Close();
2959
2960 return true;
2961}
2962
2963bool STEP_PCB_MODEL::WriteSTEP( const wxString& aFileName, bool aOptimize, bool compress )
2964{
2965 if( !isBoardOutlineValid() )
2966 {
2967 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
2968 aFileName ),
2970 return false;
2971 }
2972
2974
2975 wxFileName fn( aFileName );
2976
2977 STEPCAFControl_Writer writer;
2978 writer.SetColorMode( Standard_True );
2979 writer.SetNameMode( Standard_True );
2980
2981 // This must be set before we "transfer" the document.
2982 // Should default to kicad_pcb.general.title_block.title,
2983 // but in the meantime, defaulting to the basename of the output
2984 // target is still better than "open cascade step translter v..."
2985 // UTF8 should be ok from ISO 10303-21:2016, but... older stuff? use boring ascii
2986 if( !Interface_Static::SetCVal( "write.step.product.name", fn.GetName().ToAscii() ) )
2987 {
2988 m_reporter->Report( _( "Failed to set STEP product name, but will attempt to continue." ),
2990 }
2991
2992 // Setting write.surfacecurve.mode to 0 reduces file size and write/read times.
2993 // But there are reports that this mode might be less compatible in some cases.
2994 if( !Interface_Static::SetIVal( "write.surfacecurve.mode", aOptimize ? 0 : 1 ) )
2995 {
2996 m_reporter->Report( _( "Failed to set surface curve mode, but will attempt to continue." ),
2998 }
2999
3000 if( Standard_False == writer.Transfer( m_doc, STEPControl_AsIs ) )
3001 return false;
3002
3003 APIHeaderSection_MakeHeader hdr( writer.ChangeWriter().Model() );
3004
3005 // Note: use only Ascii7 chars, non Ascii7 chars (therefore UFT8 chars)
3006 // are creating issues in the step file
3007 hdr.SetName( new TCollection_HAsciiString( fn.GetFullName().ToAscii() ) );
3008
3009 // TODO: how to control and ensure consistency with IGES?
3010 hdr.SetAuthorValue( 1, new TCollection_HAsciiString( "Pcbnew" ) );
3011 hdr.SetOrganizationValue( 1, new TCollection_HAsciiString( "Kicad" ) );
3012 hdr.SetOriginatingSystem( new TCollection_HAsciiString( "KiCad to STEP converter" ) );
3013 hdr.SetDescriptionValue( 1, new TCollection_HAsciiString( "KiCad electronic assembly" ) );
3014
3015 bool success = true;
3016
3017 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
3018 wxString currCWD = wxGetCwd();
3019 wxString workCWD = fn.GetPath();
3020
3021 if( !workCWD.IsEmpty() )
3022 wxSetWorkingDirectory( workCWD );
3023
3024 wxString tmpfname( "$tempfile$.step" );
3025
3026 if( Standard_False == writer.Write( tmpfname.c_str() ) )
3027 success = false;
3028
3029 if( compress && success )
3030 {
3031 wxString srcTmp( tmpfname );
3032 wxString dstTmp( "$tempfile$.stpz" );
3033
3034 success = STEP_PCB_MODEL::CompressSTEP( srcTmp, dstTmp );
3035 wxRemoveFile( srcTmp );
3036
3037 tmpfname = dstTmp;
3038 }
3039
3040 if( success )
3041 {
3042
3043 // Preserve the permissions of the current file
3044 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpfname.c_str() );
3045
3046 if( !wxRenameFile( tmpfname, fn.GetFullName(), true ) )
3047 {
3048 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'." ),
3049 tmpfname,
3050 fn.GetFullName() ),
3052 success = false;
3053 }
3054 }
3055
3056 wxSetWorkingDirectory( currCWD );
3057
3058 return success;
3059}
3060
3061
3062bool STEP_PCB_MODEL::WriteBREP( const wxString& aFileName )
3063{
3064 if( !isBoardOutlineValid() )
3065 {
3066 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
3067 aFileName ),
3069 return false;
3070 }
3071
3073
3074 // s_assy = shape tool for the source
3075 Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
3076
3077 // retrieve assembly as a single shape
3078 TopoDS_Shape shape = getOneShape( s_assy );
3079
3080 wxFileName fn( aFileName );
3081
3082 wxFFileOutputStream ffStream( fn.GetFullPath() );
3083 wxStdOutputStream stdStream( ffStream );
3084
3085#if OCC_VERSION_HEX >= 0x070600
3086 BRepTools::Write( shape, stdStream, false, false, TopTools_FormatVersion_VERSION_1 );
3087#else
3088 BRepTools::Write( shape, stdStream );
3089#endif
3090
3091 return true;
3092}
3093
3094
3095bool STEP_PCB_MODEL::WriteXAO( const wxString& aFileName )
3096{
3097 wxFileName fn( aFileName );
3098
3099 wxFFileOutputStream ffStream( fn.GetFullPath() );
3100 wxStdOutputStream file( ffStream );
3101
3102 if( !ffStream.IsOk() )
3103 {
3104 m_reporter->Report( wxString::Format( "Could not open file '%s'", fn.GetFullPath() ),
3106 return false;
3107 }
3108
3110
3111 // s_assy = shape tool for the source
3112 Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( m_doc->Main() );
3113
3114 // retrieve assembly as a single shape
3115 const TopoDS_Shape shape = getOneShape( s_assy );
3116
3117 std::map<wxString, std::vector<int>> groups[4];
3118 std::map<wxString, double> groupAreas;
3119 TopExp_Explorer exp;
3120 int faceIndex = 0;
3121
3122 for( exp.Init( shape, TopAbs_FACE ); exp.More(); exp.Next() )
3123 {
3124 TopoDS_Shape subShape = exp.Current();
3125
3126 Bnd_Box bbox;
3127 BRepBndLib::Add( subShape, bbox );
3128
3129 for( const auto& [padKey, pairs] : m_pad_points )
3130 {
3131 for( const auto& pair : pairs )
3132 {
3133 const auto& [point, padTestShape] = pair;
3134
3135 if( bbox.IsOut( point ) )
3136 continue;
3137
3138 BRepAdaptor_Surface surface( TopoDS::Face( subShape ) );
3139
3140 if( surface.GetType() != GeomAbs_Plane )
3141 continue;
3142
3143 BRepExtrema_DistShapeShape dist( padTestShape, subShape );
3144 dist.Perform();
3145
3146 if( !dist.IsDone() )
3147 continue;
3148
3149 if( dist.Value() < Precision::Approximation() )
3150 {
3151 // Push as a face group
3152 groups[2][padKey].push_back( faceIndex );
3153
3154 GProp_GProps system;
3155 BRepGProp::SurfaceProperties( subShape, system );
3156
3157 double surfaceArea = system.Mass() / 1e6; // Convert to meters^2
3158 groupAreas[padKey] += surfaceArea;
3159 }
3160 }
3161 }
3162
3163 faceIndex++;
3164 }
3165
3166 // Based on Gmsh code
3167 file << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << std::endl;
3168 file << "<XAO version=\"1.0\" author=\"KiCad\">" << std::endl;
3169 file << " <geometry name=\"" << fn.GetName() << "\">" << std::endl;
3170 file << " <shape format=\"BREP\"><![CDATA[";
3171#if OCC_VERSION_HEX < 0x070600
3172 BRepTools::Write( shape, file );
3173#else
3174 BRepTools::Write( shape, file, Standard_True, Standard_True, TopTools_FormatVersion_VERSION_1 );
3175#endif
3176 file << "]]></shape>" << std::endl;
3177 file << " <topology>" << std::endl;
3178
3179 TopTools_IndexedMapOfShape mainMap;
3180 TopExp::MapShapes( shape, mainMap );
3181 std::set<int> topo[4];
3182
3183 static const TopAbs_ShapeEnum c_dimShapeTypes[] = { TopAbs_VERTEX, TopAbs_EDGE, TopAbs_FACE,
3184 TopAbs_SOLID };
3185
3186 static const std::string c_dimLabel[] = { "vertex", "edge", "face", "solid" };
3187 static const std::string c_dimLabels[] = { "vertices", "edges", "faces", "solids" };
3188
3189 for( int dim = 0; dim < 4; dim++ )
3190 {
3191 for( exp.Init( shape, c_dimShapeTypes[dim] ); exp.More(); exp.Next() )
3192 {
3193 TopoDS_Shape subShape = exp.Current();
3194 int idx = mainMap.FindIndex( subShape );
3195
3196 if( idx && !topo[dim].count( idx ) )
3197 topo[dim].insert( idx );
3198 }
3199 }
3200
3201 for( int dim = 0; dim <= 3; dim++ )
3202 {
3203 std::string labels = c_dimLabels[dim];
3204 std::string label = c_dimLabel[dim];
3205
3206 file << " <" << labels << " count=\"" << topo[dim].size() << "\">" << std::endl;
3207 int index = 0;
3208
3209 for( auto p : topo[dim] )
3210 {
3211 std::string name( "" );
3212 file << " <" << label << " index=\"" << index << "\" "
3213 << "name=\"" << name << "\" "
3214 << "reference=\"" << p << "\"/>" << std::endl;
3215
3216 index++;
3217 }
3218 file << " </" << labels << ">" << std::endl;
3219 }
3220
3221 file << " </topology>" << std::endl;
3222 file << " </geometry>" << std::endl;
3223 file << " <groups count=\""
3224 << groups[0].size() + groups[1].size() + groups[2].size() + groups[3].size() << "\">"
3225 << std::endl;
3226
3227 int groupNumber = 1;
3228
3229 m_reporter->Report( wxT( "Pad definitions:" ), RPT_SEVERITY_DEBUG );
3230 m_reporter->Report( wxT( "Number\tName\tArea (m^2)" ), RPT_SEVERITY_DEBUG );
3231
3232 for( int dim = 0; dim <= 3; dim++ )
3233 {
3234 std::string label = c_dimLabel[dim];
3235
3236 for( auto g : groups[dim] )
3237 {
3238 //std::string name = model->getPhysicalName( dim, g.first );
3239 wxString name = g.first;
3240
3241 if( name.empty() )
3242 { // create same unique name as for MED export
3243 std::ostringstream gs;
3244 gs << "G_" << dim << "D_" << g.first;
3245 name = gs.str();
3246 }
3247 file << " <group name=\"" << name << "\" dimension=\"" << label;
3248//#if 1
3249// // Gmsh XAO extension: also save the physical tag, so that XAO can be used
3250// // to serialize OCC geometries, ready to be used by GetDP, GmshFEM & co
3251// file << "\" tag=\"" << g.first;
3252//#endif
3253 file << "\" count=\"" << g.second.size() << "\">" << std::endl;
3254
3255 for( auto index : g.second )
3256 file << " <element index=\"" << index << "\"/>" << std::endl;
3257
3258 file << " </group>" << std::endl;
3259
3260 m_reporter->Report( wxString::Format( "%d\t%s\t%g",
3261 groupNumber,
3262 name,
3263 groupAreas[name] ),
3265
3266 groupNumber++;
3267 }
3268 }
3269
3270 m_reporter->Report( wxT( "" ), RPT_SEVERITY_DEBUG );
3271
3272 file << " </groups>" << std::endl;
3273 file << " <fields count=\"0\"/>" << std::endl;
3274 file << "</XAO>" << std::endl;
3275
3276 return true;
3277}
3278
3279
3280bool STEP_PCB_MODEL::getModelLabel( const wxString& aBaseName, const wxString& aFileName,
3281 const std::vector<wxString>& aAltFilenames, VECTOR3D aScale,
3282 TDF_Label& aLabel, bool aSubstituteModels,
3283 wxString* aErrorMessage )
3284{
3285 std::string fileNameUTF8 = aFileName.utf8_string();
3286
3287 std::string model_key = fileNameUTF8 + "_" + std::to_string( aScale.x ) + "_"
3288 + std::to_string( aScale.y ) + "_" + std::to_string( aScale.z );
3289
3290 MODEL_MAP::const_iterator mm = m_models.find( model_key );
3291
3292 if( mm != m_models.end() )
3293 {
3294 aLabel = mm->second;
3295 return true;
3296 }
3297
3298 aLabel.Nullify();
3299
3300 Handle( TDocStd_Document ) doc;
3301 m_app->NewDocument( "MDTV-XCAF", doc );
3302
3303 MODEL3D_FORMAT_TYPE modelFmt = fileType( fileNameUTF8.c_str() );
3304 TCollection_ExtendedString partname( aBaseName.utf8_str() );
3305
3306 switch( modelFmt )
3307 {
3308 case FMT_IGES:
3309 if( !readIGES( doc, fileNameUTF8.c_str() ) )
3310 {
3311 m_reporter->Report( wxString::Format( wxT( "readIGES() failed on filename '%s'." ), aFileName ),
3313 return false;
3314 }
3315
3316 break;
3317
3318 case FMT_STEP:
3319 if( !readSTEP( doc, fileNameUTF8.c_str() ) )
3320 {
3321 m_reporter->Report( wxString::Format( wxT( "readSTEP() failed on filename '%s'." ), aFileName ),
3323 return false;
3324 }
3325
3326 break;
3327
3328 case FMT_STEPZ:
3329 {
3330 // To export a compressed step file (.stpz or .stp.gz file), the best way is to
3331 // decaompress it in a temporaty file and load this temporary file
3332 wxFFileInputStream ifile( aFileName );
3333 wxFileName outFile( aFileName );
3334
3335 outFile.SetPath( wxStandardPaths::Get().GetTempDir() );
3336 outFile.SetExt( wxT( "step" ) );
3337 wxFileOffset size = ifile.GetLength();
3338
3339 if( size == wxInvalidOffset )
3340 {
3341 m_reporter->Report( wxString::Format( wxT( "getModelLabel() failed on filename '%s'." ),
3342 aFileName ),
3344 return false;
3345 }
3346
3347 {
3348 bool success = false;
3349
3350 {
3351 wxFFileOutputStream ofile( outFile.GetFullPath() );
3352
3353 if( !ofile.IsOk() )
3354 return false;
3355
3356 char* buffer = new char[size];
3357
3358 ifile.Read( buffer, size );
3359 std::string expanded;
3360
3361 try
3362 {
3363 expanded = gzip::decompress( buffer, size );
3364 success = true;
3365 }
3366 catch( ... )
3367 {
3368 m_reporter->Report(
3369 wxString::Format( wxT( "failed to decompress '%s'." ), aFileName ),
3371 }
3372
3373 if( expanded.empty() )
3374 {
3375 ifile.Reset();
3376 ifile.SeekI( 0 );
3377 wxZipInputStream izipfile( ifile );
3378 std::unique_ptr<wxZipEntry> zip_file( izipfile.GetNextEntry() );
3379
3380 if( zip_file && !zip_file->IsDir() && izipfile.CanRead() )
3381 {
3382 izipfile.Read( ofile );
3383 success = true;
3384 }
3385 }
3386 else
3387 {
3388 ofile.Write( expanded.data(), expanded.size() );
3389 }
3390
3391 delete[] buffer;
3392 }
3393
3394 if( success )
3395 {
3396 success = getModelLabel( aBaseName, outFile.GetFullPath(), aAltFilenames,
3397 VECTOR3D( 1.0, 1.0, 1.0 ), aLabel, false );
3398 }
3399
3400 return success;
3401 }
3402
3403 break;
3404 }
3405
3406 case FMT_WRL:
3407 case FMT_WRZ:
3408 /* WRL files are preferred for internal rendering, due to superior material properties, etc.
3409 * However they are not suitable for MCAD export.
3410 *
3411 * If a .wrl file is specified, attempt to locate a replacement file for it.
3412 *
3413 * If a valid replacement file is found, the label for THAT file will be associated with
3414 * the .wrl file
3415 */
3416 if( aSubstituteModels )
3417 {
3418 wxFileName wrlName( aFileName );
3419
3420 wxString basePath = wrlName.GetPath();
3421 wxString baseName = wrlName.GetName();
3422
3423 // List of alternate files to look for
3424 // Given in order of preference
3425 // (Break if match is found)
3426 wxArrayString alts;
3427
3428 // Step files
3429 alts.Add( wxT( "stp" ) );
3430 alts.Add( wxT( "step" ) );
3431 alts.Add( wxT( "STP" ) );
3432 alts.Add( wxT( "STEP" ) );
3433 alts.Add( wxT( "Stp" ) );
3434 alts.Add( wxT( "Step" ) );
3435 alts.Add( wxT( "stpz" ) );
3436 alts.Add( wxT( "stpZ" ) );
3437 alts.Add( wxT( "STPZ" ) );
3438 alts.Add( wxT( "step.gz" ) );
3439 alts.Add( wxT( "stp.gz" ) );
3440
3441 // IGES files
3442 alts.Add( wxT( "iges" ) );
3443 alts.Add( wxT( "IGES" ) );
3444 alts.Add( wxT( "igs" ) );
3445 alts.Add( wxT( "IGS" ) );
3446
3447 //TODO - Other alternative formats?
3448
3449 for( const auto& altExt : alts )
3450 {
3451 wxFileName altFile;
3452
3453 if( !aAltFilenames.empty() )
3454 {
3455 for( const wxString& altPath : aAltFilenames )
3456 {
3457 wxFileName iterFn( altPath );
3458
3459 if( iterFn.GetExt() == altExt )
3460 {
3461 altFile = iterFn;
3462 break;
3463 }
3464 }
3465 }
3466 else
3467 {
3468 altFile = wxFileName( basePath, baseName + wxT( "." ) + altExt );
3469 }
3470
3471 if( altFile.IsOk() && altFile.FileExists() )
3472 {
3473 // When substituting a STEP/IGS file for VRML, do not apply the VRML scaling
3474 // to the new STEP model. This process of auto-substitution is janky as all
3475 // heck so let's not mix up un-displayed scale factors with potentially
3476 // mis-matched files. And hope that the user doesn't have multiples files
3477 // named "model.wrl" and "model.stp" referring to different parts.
3478 // TODO: Fix model handling in v7. Default models should only be STP.
3479 // Have option to override this in DISPLAY.
3480 if( getModelLabel( aBaseName, altFile.GetFullPath(), {},
3481 VECTOR3D( 1.0, 1.0, 1.0 ), aLabel, false ) )
3482 {
3483 return true;
3484 }
3485 }
3486 }
3487 }
3488
3489 // VRML models only work when exporting to mesh formats
3490 // Also OCCT < 7.9.0 fails to load most VRML 2.0 models because of Switch nodes
3494 {
3495 if( readVRML( doc, fileNameUTF8.c_str() ) )
3496 {
3497 Handle( XCAFDoc_ShapeTool ) shapeTool =
3498 XCAFDoc_DocumentTool::ShapeTool( doc->Main() );
3499
3500 prefixNames( shapeTool->Label(), partname );
3501 }
3502 else
3503 {
3504 m_reporter->Report(
3505 wxString::Format( wxT( "readVRML() failed on filename '%s'." ),
3506 aFileName ),
3508
3509 return false;
3510 }
3511 }
3512 else // Substitution is not allowed
3513 {
3514 if( aErrorMessage )
3515 aErrorMessage->Printf( _( "Cannot use VRML models when exporting to non-mesh formats." ) );
3516
3517 return false;
3518 }
3519
3520 break;
3521
3522 // TODO: implement IDF and EMN converters
3523
3524 default:
3525 m_reporter->Report( wxString::Format( _( "Cannot identify actual file type for '%s'." ), aFileName ),
3527 return false;
3528 }
3529
3530 aLabel = transferModel( doc, m_doc, aScale );
3531
3532 if( aLabel.IsNull() )
3533 {
3534 m_reporter->Report( wxString::Format( _( "Could not transfer model data from file '%s'." ), aFileName ),
3536 return false;
3537 }
3538
3539 // attach the PART NAME ( base filename: note that in principle
3540 // different models may have the same base filename )
3541 TDataStd_Name::Set( aLabel, partname );
3542
3543 m_models.insert( MODEL_DATUM( model_key, aLabel ) );
3544 ++m_components;
3545 return true;
3546}
3547
3548
3549bool STEP_PCB_MODEL::getModelLocation( bool aBottom, const VECTOR2D& aPosition, double aRotation,
3550 const VECTOR3D& aOffset, const VECTOR3D& aOrientation,
3551 TopLoc_Location& aLocation )
3552{
3553 // Order of operations:
3554 // a. aOrientation is applied -Z*-Y*-X
3555 // b. aOffset is applied
3556 // Top ? add thickness to the Z offset
3557 // c. Bottom ? Rotate on X axis (in contrast to most ECAD which mirror on Y),
3558 // then rotate on +Z
3559 // Top ? rotate on -Z
3560 // d. aPosition is applied
3561 //
3562 // Note: Y axis is inverted in KiCad
3563
3564 gp_Trsf lPos;
3565 lPos.SetTranslation( gp_Vec( aPosition.x, -aPosition.y, 0.0 ) );
3566
3567 // Offset board thickness
3568 VECTOR3D offset( aOffset );
3569 offset.z += BOARD_OFFSET;
3570
3571 double boardThickness;
3572 double boardZPos;
3573 getBoardBodyZPlacement( boardZPos, boardThickness );
3574 double top = std::max( boardZPos, boardZPos + boardThickness );
3575 double bottom = std::min( boardZPos, boardZPos + boardThickness );
3576
3577 // 3D step models are placed on the top of copper layers.
3578 // This is true for SMD shapes, and perhaps not always true for TH shapes,
3579 // but we use this Z position for any 3D shape.
3580 double f_pos, f_thickness;
3581 getLayerZPlacement( F_Cu, f_pos, f_thickness );
3582 top += f_thickness;
3583 getLayerZPlacement( B_Cu, f_pos, f_thickness );
3584 bottom += f_thickness; // f_thickness is < 0 for B_Cu layer
3585
3586 gp_Trsf lRot;
3587
3588 if( aBottom )
3589 {
3590 offset.z -= bottom;
3591 lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), aRotation );
3592 lPos.Multiply( lRot );
3593 lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 1.0, 0.0, 0.0 ) ), M_PI );
3594 lPos.Multiply( lRot );
3595 }
3596 else
3597 {
3598 offset.z += top;
3599 lRot.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), aRotation );
3600 lPos.Multiply( lRot );
3601 }
3602
3603 gp_Trsf lOff;
3604 lOff.SetTranslation( gp_Vec( offset.x, offset.y, offset.z ) );
3605 lPos.Multiply( lOff );
3606
3607 gp_Trsf lOrient;
3608 lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 0.0, 1.0 ) ), -aOrientation.z );
3609 lPos.Multiply( lOrient );
3610 lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 0.0, 1.0, 0.0 ) ), -aOrientation.y );
3611 lPos.Multiply( lOrient );
3612 lOrient.SetRotation( gp_Ax1( gp_Pnt( 0.0, 0.0, 0.0 ), gp_Dir( 1.0, 0.0, 0.0 ) ), -aOrientation.x );
3613 lPos.Multiply( lOrient );
3614
3615 aLocation = TopLoc_Location( lPos );
3616 return true;
3617}
3618
3619
3620bool STEP_PCB_MODEL::readIGES( Handle( TDocStd_Document )& doc, const char* fname )
3621{
3622 IGESControl_Controller::Init();
3623 IGESCAFControl_Reader reader;
3624 IFSelect_ReturnStatus stat = reader.ReadFile( fname );
3625
3626 if( stat != IFSelect_RetDone )
3627 return false;
3628
3629 // Enable user-defined shape precision
3630 if( !Interface_Static::SetIVal( "read.precision.mode", 1 ) )
3631 return false;
3632
3633 // Set the shape conversion precision to USER_PREC (default 0.0001 has too many triangles)
3634 if( !Interface_Static::SetRVal( "read.precision.val", USER_PREC ) )
3635 return false;
3636
3637 // set other translation options
3638 reader.SetColorMode( true ); // use model colors
3639 reader.SetNameMode( false ); // don't use IGES label names
3640 reader.SetLayerMode( false ); // ignore LAYER data
3641
3642 if( !reader.Transfer( doc ) )
3643 {
3644 if( doc->CanClose() == CDM_CCS_OK )
3645 doc->Close();
3646
3647 return false;
3648 }
3649
3650 // are there any shapes to translate?
3651 if( reader.NbShapes() < 1 )
3652 {
3653 if( doc->CanClose() == CDM_CCS_OK )
3654 doc->Close();
3655
3656 return false;
3657 }
3658
3659 return true;
3660}
3661
3662
3663bool STEP_PCB_MODEL::readSTEP( Handle( TDocStd_Document )& doc, const char* fname )
3664{
3665 STEPCAFControl_Reader reader;
3666 IFSelect_ReturnStatus stat = reader.ReadFile( fname );
3667
3668 if( stat != IFSelect_RetDone )
3669 return false;
3670
3671 // Enable user-defined shape precision
3672 if( !Interface_Static::SetIVal( "read.precision.mode", 1 ) )
3673 return false;
3674
3675 // Set the shape conversion precision to USER_PREC (default 0.0001 has too many triangles)
3676 if( !Interface_Static::SetRVal( "read.precision.val", USER_PREC ) )
3677 return false;
3678
3679 // set other translation options
3680 reader.SetColorMode( true ); // use model colors
3681 reader.SetNameMode( true ); // use label names
3682 reader.SetLayerMode( false ); // ignore LAYER data
3683
3684 if( !reader.Transfer( doc ) )
3685 {
3686 if( doc->CanClose() == CDM_CCS_OK )
3687 doc->Close();
3688
3689 return false;
3690 }
3691
3692 // are there any shapes to translate?
3693 if( reader.NbRootsForTransfer() < 1 )
3694 {
3695 if( doc->CanClose() == CDM_CCS_OK )
3696 doc->Close();
3697
3698 return false;
3699 }
3700
3701 return true;
3702}
3703
3704
3705bool STEP_PCB_MODEL::readVRML( Handle( TDocStd_Document ) & doc, const char* fname )
3706{
3707#if OCC_VERSION_HEX >= 0x070700
3708 VrmlAPI_CafReader reader;
3709 RWMesh_CoordinateSystemConverter conv;
3710 conv.SetInputLengthUnit( 2.54 );
3711 reader.SetCoordinateSystemConverter( conv );
3712 reader.SetDocument( doc );
3713
3714 if( !reader.Perform( TCollection_AsciiString( fname ), Message_ProgressRange() ) )
3715 return false;
3716
3717 return true;
3718#else
3719 return false;
3720#endif
3721}
3722
3723
3724void STEP_PCB_MODEL::transferColors( Handle( XCAFDoc_ShapeTool )& aSrcShapeTool,
3725 Handle( XCAFDoc_ColorTool )& aSrcColorTool,
3726 Handle( XCAFDoc_ShapeTool )& aDstShapeTool,
3727 Handle( XCAFDoc_ColorTool )& aDstColorTool )
3728{
3729 // Get all shapes from the source document
3730 TDF_LabelSequence srcLabels;
3731 aSrcShapeTool->GetShapes( srcLabels );
3732
3733 for( Standard_Integer i = 1; i <= srcLabels.Length(); i++ )
3734 {
3735 TDF_Label srcLabel = srcLabels.Value( i );
3736 TopoDS_Shape srcShape = aSrcShapeTool->GetShape( srcLabel );
3737
3738 if( srcShape.IsNull() )
3739 continue;
3740
3741 // Try to find the same shape in the destination document
3742 TDF_Label dstLabel;
3743
3744 if( !aDstShapeTool->Search( srcShape, dstLabel, Standard_True, Standard_True, Standard_False ) )
3745 continue;
3746
3747 // Transfer surface color
3748 Quantity_ColorRGBA surfColor;
3749
3750 if( aSrcColorTool->GetColor( srcLabel, XCAFDoc_ColorSurf, surfColor ) )
3751 aDstColorTool->SetColor( dstLabel, surfColor, XCAFDoc_ColorSurf );
3752
3753 // Transfer curve color
3754 Quantity_ColorRGBA curvColor;
3755
3756 if( aSrcColorTool->GetColor( srcLabel, XCAFDoc_ColorCurv, curvColor ) )
3757 aDstColorTool->SetColor( dstLabel, curvColor, XCAFDoc_ColorCurv );
3758
3759 // Transfer generic color
3760 Quantity_ColorRGBA genColor;
3761
3762 if( aSrcColorTool->GetColor( srcLabel, XCAFDoc_ColorGen, genColor ) )
3763 aDstColorTool->SetColor( dstLabel, genColor, XCAFDoc_ColorGen );
3764
3765 // Also check for colors on individual faces
3766 if( aSrcShapeTool->IsSimpleShape( srcLabel ) )
3767 {
3768 TopoDS_Shape shape = aSrcShapeTool->GetShape( srcLabel );
3769
3770 for( TopExp_Explorer exp( shape, TopAbs_FACE ); exp.More(); exp.Next() )
3771 {
3772 TopoDS_Face face = TopoDS::Face( exp.Current() );
3773 Quantity_ColorRGBA faceColor;
3774
3775 if( aSrcColorTool->GetColor( face, XCAFDoc_ColorSurf, faceColor ) )
3776 aDstColorTool->SetColor( face, faceColor, XCAFDoc_ColorSurf );
3777 else if( aSrcColorTool->GetColor( face, XCAFDoc_ColorGen, faceColor ) )
3778 aDstColorTool->SetColor( face, faceColor, XCAFDoc_ColorGen );
3779 }
3780 }
3781 }
3782
3783 // Also iterate through subshapes and components recursively
3784 TDF_LabelSequence srcFreeShapes;
3785 aSrcShapeTool->GetFreeShapes( srcFreeShapes );
3786
3787 std::function<void( const TDF_Label& )> transferColorsRecursive = [&]( const TDF_Label& aLabel )
3788 {
3789 TopoDS_Shape shape = aSrcShapeTool->GetShape( aLabel );
3790
3791 if( shape.IsNull() )
3792 return;
3793
3794 // Find this shape in destination
3795 TDF_Label dstLabel;
3796
3797 if( aDstShapeTool->Search( shape, dstLabel, Standard_True, Standard_True, Standard_False ) )
3798 {
3799 Quantity_ColorRGBA color;
3800
3801 if( aSrcColorTool->GetColor( aLabel, XCAFDoc_ColorSurf, color ) )
3802 aDstColorTool->SetColor( dstLabel, color, XCAFDoc_ColorSurf );
3803
3804 if( aSrcColorTool->GetColor( aLabel, XCAFDoc_ColorCurv, color ) )
3805 aDstColorTool->SetColor( dstLabel, color, XCAFDoc_ColorCurv );
3806
3807 if( aSrcColorTool->GetColor( aLabel, XCAFDoc_ColorGen, color ) )
3808 aDstColorTool->SetColor( dstLabel, color, XCAFDoc_ColorGen );
3809 }
3810
3811 // Process children
3812 for( TDF_ChildIterator it( aLabel ); it.More(); it.Next() )
3813 transferColorsRecursive( it.Value() );
3814
3815 // Process components if this is an assembly
3816 if( aSrcShapeTool->IsAssembly( aLabel ) )
3817 {
3818 TDF_LabelSequence components;
3819 aSrcShapeTool->GetComponents( aLabel, components );
3820
3821 for( Standard_Integer j = 1; j <= components.Length(); j++ )
3822 {
3823 TDF_Label compLabel = components.Value( j );
3824 TDF_Label refLabel;
3825
3826 if( aSrcShapeTool->GetReferredShape( compLabel, refLabel ) )
3827 transferColorsRecursive( refLabel );
3828 }
3829 }
3830 };
3831
3832 for( Standard_Integer i = 1; i <= srcFreeShapes.Length(); i++ )
3833 transferColorsRecursive( srcFreeShapes.Value( i ) );
3834}
3835
3836
3837TDF_Label STEP_PCB_MODEL::transferModel( Handle( TDocStd_Document ) & source,
3838 Handle( TDocStd_Document ) & dest, const VECTOR3D& aScale )
3839{
3840 // transfer data from Source into a top level component of Dest
3841 // s_assy = shape tool for the source
3842 Handle( XCAFDoc_ShapeTool ) s_assy = XCAFDoc_DocumentTool::ShapeTool( source->Main() );
3843 Handle( XCAFDoc_ColorTool ) s_color = XCAFDoc_DocumentTool::ColorTool( source->Main() );
3844
3845 // retrieve all free shapes within the assembly
3846 TDF_LabelSequence frshapes;
3847 s_assy->GetFreeShapes( frshapes );
3848
3849 // d_assy = shape tool for the destination
3850 Handle( XCAFDoc_ShapeTool ) d_assy = XCAFDoc_DocumentTool::ShapeTool( dest->Main() );
3851 Handle( XCAFDoc_ColorTool ) d_color = XCAFDoc_DocumentTool::ColorTool( dest->Main() );
3852
3853 // create a new shape within the destination and set the assembly tool to point to it
3854 TDF_Label d_targetLabel = d_assy->NewShape();
3855
3856 auto copyLabel = [&]( TDF_Label& d_label, const TDF_Label& s_label ) -> bool
3857 {
3858 // TDocStd_XLinkTool::Copy requires the source to be "self-contained", meaning it has
3859 // no external references. Some STEP files (e.g. from Fusion 360 with linked components)
3860 // may contain internal references that violate this constraint. In such cases, we fall
3861 // back to extracting just the geometric shape without the full XDE document structure.
3862 if( TDF_Tool::IsSelfContained( s_label ) )
3863 {
3864 TDocStd_XLinkTool link;
3865 link.Copy( d_label, s_label );
3866 return true;
3867 }
3868 else
3869 {
3870 // The source label is not self-contained. Extract the shape directly.
3871 TopoDS_Shape shape = s_assy->GetShape( s_label );
3872
3873 if( shape.IsNull() )
3874 return false;
3875
3876 // Add the shape directly without the XDE structure. This loses some metadata
3877 // like colors and names, but allows the model to be successfully transferred.
3878 d_assy->SetShape( d_label, shape );
3879
3880 m_reporter->Report( wxT( "Model contains non-self-contained data; some metadata may be lost." ),
3882 return true;
3883 }
3884 };
3885
3886 if( frshapes.Size() == 1 )
3887 {
3888 if( !copyLabel( d_targetLabel, frshapes.First() ) )
3889 {
3890 m_reporter->Report( wxT( "Failed to transfer model." ), RPT_SEVERITY_ERROR );
3891 return TDF_Label();
3892 }
3893 }
3894 else
3895 {
3896 // Rare case with multiple free shapes
3897 for( TDF_Label& s_shapeLabel : frshapes )
3898 {
3899 TDF_Label d_component = d_assy->NewShape();
3900
3901 if( !copyLabel( d_component, s_shapeLabel ) )
3902 {
3903 m_reporter->Report( wxT( "Failed to transfer model component." ), RPT_SEVERITY_ERROR );
3904 return TDF_Label();
3905 }
3906
3907 d_assy->AddComponent( d_targetLabel, d_component, TopLoc_Location() );
3908 }
3909 }
3910
3911 // Transfer colors from source to destination document
3912 // This is necessary because TDocStd_XLinkTool::Copy may not properly transfer
3913 // color associations which are stored separately in the ColorTool section
3914 transferColors( s_assy, s_color, d_assy, d_color );
3915
3916 if( aScale.x != 1.0 || aScale.y != 1.0 || aScale.z != 1.0 )
3917 rescaleShapes( d_targetLabel, gp_XYZ( aScale.x, aScale.y, aScale.z ) );
3918
3919 return d_targetLabel;
3920}
3921
3922
3923bool STEP_PCB_MODEL::performMeshing( Handle( XCAFDoc_ShapeTool ) & aShapeTool )
3924{
3925 TDF_LabelSequence freeShapes;
3926 aShapeTool->GetFreeShapes( freeShapes );
3927
3928 m_reporter->Report( wxT( "Meshing model" ), RPT_SEVERITY_DEBUG );
3929
3930 // GLTF is a mesh format, we have to trigger opencascade to mesh the shapes we composited into the asesmbly
3931 // To mesh models, lets just grab the free shape root and execute on them
3932 for( Standard_Integer i = 1; i <= freeShapes.Length(); ++i )
3933 {
3934 TDF_Label label = freeShapes.Value( i );
3935 TopoDS_Shape shape;
3936 aShapeTool->GetShape( label, shape );
3937
3938 // These deflection values basically affect the accuracy of the mesh generated, a tighter
3939 // deflection will result in larger meshes
3940 // We could make this a tunable parameter, but for now fix it
3941 const Standard_Real linearDeflection = 0.14;
3942 const Standard_Real angularDeflection = DEG2RAD( 30.0 );
3943 BRepMesh_IncrementalMesh mesh( shape, linearDeflection, Standard_False, angularDeflection,
3944 Standard_True );
3945 }
3946
3947 return true;
3948}
3949
3950
3951bool STEP_PCB_MODEL::WriteGLTF( const wxString& aFileName )
3952{
3953 /*if( !isBoardOutlineValid() )
3954 {
3955 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
3956 aFileName ),
3957 RPT_SEVERITY_ERROR );
3958 return false;
3959 }*/
3960
3962
3963 performMeshing( m_assy );
3964
3965 wxFileName fn( aFileName );
3966
3967 const char* tmpGltfname = "$tempfile$.glb";
3968 RWGltf_CafWriter cafWriter( tmpGltfname, true );
3969
3970 cafWriter.SetTransformationFormat( RWGltf_WriterTrsfFormat_Compact );
3971 cafWriter.ChangeCoordinateSystemConverter().SetInputLengthUnit( 0.001 );
3972 cafWriter.ChangeCoordinateSystemConverter().SetInputCoordinateSystem(
3973 RWMesh_CoordinateSystem_Zup );
3974#if OCC_VERSION_HEX >= 0x070700
3975 cafWriter.SetParallel( true );
3976#endif
3977 TColStd_IndexedDataMapOfStringString metadata;
3978
3979 metadata.Add( TCollection_AsciiString( "pcb_name" ),
3980 TCollection_ExtendedString( fn.GetName().wc_str() ) );
3981 metadata.Add( TCollection_AsciiString( "source_pcb_file" ),
3982 TCollection_ExtendedString( fn.GetFullName().wc_str() ) );
3983 metadata.Add( TCollection_AsciiString( "generator" ),
3984 TCollection_AsciiString( wxString::Format( wxS( "KiCad %s" ), GetSemanticVersion() ).ToAscii() ) );
3985 metadata.Add( TCollection_AsciiString( "generated_at" ),
3986 TCollection_AsciiString( GetISO8601CurrentDateTime().ToAscii() ) );
3987
3988 bool success = true;
3989
3990 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
3991 wxString currCWD = wxGetCwd();
3992 wxString workCWD = fn.GetPath();
3993
3994 if( !workCWD.IsEmpty() )
3995 wxSetWorkingDirectory( workCWD );
3996
3997 success = cafWriter.Perform( m_doc, metadata, Message_ProgressRange() );
3998
3999 if( success )
4000 {
4001 // Preserve the permissions of the current file
4002 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpGltfname );
4003
4004 if( !wxRenameFile( tmpGltfname, fn.GetFullName(), true ) )
4005 {
4006 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'." ),
4007 tmpGltfname,
4008 fn.GetFullName() ),
4010 success = false;
4011 }
4012 }
4013
4014 wxSetWorkingDirectory( currCWD );
4015
4016 return success;
4017}
4018
4019
4020bool STEP_PCB_MODEL::WritePLY( const wxString& aFileName )
4021{
4022#if OCC_VERSION_HEX < 0x070700
4023 m_reporter->Report( wxT( "PLY export is not supported before OCCT 7.7.0" ), RPT_SEVERITY_ERROR );
4024 return false;
4025#else
4026
4027 if( !isBoardOutlineValid() )
4028 {
4029 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
4030 aFileName ),
4032 return false;
4033 }
4034
4036
4037 performMeshing( m_assy );
4038
4039 wxFileName fn( aFileName );
4040
4041 const char* tmpFname = "$tempfile$.ply";
4042 RWPly_CafWriter cafWriter( tmpFname );
4043
4044 cafWriter.SetFaceId( true ); // TODO: configurable SetPartId/SetFaceId
4045 cafWriter.ChangeCoordinateSystemConverter().SetInputLengthUnit( 0.001 );
4046 cafWriter.ChangeCoordinateSystemConverter().SetInputCoordinateSystem( RWMesh_CoordinateSystem_Zup );
4047
4048 TColStd_IndexedDataMapOfStringString metadata;
4049
4050 metadata.Add( TCollection_AsciiString( "pcb_name" ),
4051 TCollection_ExtendedString( fn.GetName().wc_str() ) );
4052 metadata.Add( TCollection_AsciiString( "source_pcb_file" ),
4053 TCollection_ExtendedString( fn.GetFullName().wc_str() ) );
4054 metadata.Add( TCollection_AsciiString( "generator" ),
4055 TCollection_AsciiString( wxString::Format( wxS( "KiCad %s" ),
4056 GetSemanticVersion() ).ToAscii() ) );
4057 metadata.Add( TCollection_AsciiString( "generated_at" ),
4058 TCollection_AsciiString( GetISO8601CurrentDateTime().ToAscii() ) );
4059
4060 bool success = true;
4061
4062 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
4063 wxString currCWD = wxGetCwd();
4064 wxString workCWD = fn.GetPath();
4065
4066 if( !workCWD.IsEmpty() )
4067 wxSetWorkingDirectory( workCWD );
4068
4069 success = cafWriter.Perform( m_doc, metadata, Message_ProgressRange() );
4070
4071 if( success )
4072 {
4073 // Preserve the permissions of the current file
4074 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpFname );
4075
4076 if( !wxRenameFile( tmpFname, fn.GetFullName(), true ) )
4077 {
4078 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'." ),
4079 tmpFname,
4080 fn.GetFullName() ),
4082 success = false;
4083 }
4084 }
4085
4086 wxSetWorkingDirectory( currCWD );
4087
4088 return success;
4089#endif
4090}
4091
4092
4093bool STEP_PCB_MODEL::WriteSTL( const wxString& aFileName )
4094{
4095 if( !isBoardOutlineValid() )
4096 {
4097 m_reporter->Report( wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'." ),
4098 aFileName ),
4100 return false;
4101 }
4102
4104
4105 performMeshing( m_assy );
4106
4107 wxFileName fn( aFileName );
4108
4109 const char* tmpFname = "$tempfile$.stl";
4110
4111 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
4112 wxString currCWD = wxGetCwd();
4113 wxString workCWD = fn.GetPath();
4114
4115 if( !workCWD.IsEmpty() )
4116 wxSetWorkingDirectory( workCWD );
4117
4118 bool success = StlAPI_Writer().Write( getOneShape( m_assy ), tmpFname );
4119
4120 if( success )
4121 {
4122 // Preserve the permissions of the current file
4123 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpFname );
4124
4125 if( !wxRenameFile( tmpFname, fn.GetFullName(), true ) )
4126 {
4127 m_reporter->Report( wxString::Format( _( "Cannot rename temporary file '%s' to '%s'." ),
4128 tmpFname,
4129 fn.GetFullName() ),
4131 success = false;
4132 }
4133 }
4134
4135 wxSetWorkingDirectory( currCWD );
4136
4137 return success;
4138}
4139
4140
4141
4142bool STEP_PCB_MODEL::WriteU3D( const wxString& aFileName )
4143{
4144 if( !isBoardOutlineValid() )
4145 {
4146 m_reporter->Report(
4147 wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'.\n" ), aFileName ),
4149 return false;
4150 }
4151
4153
4154 performMeshing( m_assy );
4155
4156 wxFileName fn( aFileName );
4157
4158 const char* tmpFname = "$tempfile$.u3d";
4159
4160 // Creates a temporary file with a ascii7 name, because writer does not know unicode filenames.
4161 wxString currCWD = wxGetCwd();
4162 wxString workCWD = fn.GetPath();
4163
4164 if( !workCWD.IsEmpty() )
4165 wxSetWorkingDirectory( workCWD );
4166
4167 U3D::WRITER writer( tmpFname );
4168 bool success = writer.Perform( m_doc );
4169 if( success )
4170 {
4171 // Preserve the permissions of the current file
4172 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), tmpFname );
4173
4174 if( !wxRenameFile( tmpFname, fn.GetFullName(), true ) )
4175 {
4176 m_reporter->Report( wxString::Format( wxT( "Cannot rename temporary file '%s' to '%s'.\n" ), tmpFname,
4177 fn.GetFullName() ),
4179 success = false;
4180 }
4181 }
4182
4183 wxSetWorkingDirectory( currCWD );
4184
4185 return success;
4186}
4187
4188
4189bool STEP_PCB_MODEL::WritePDF( const wxString& aFileName )
4190{
4191 if( !isBoardOutlineValid() )
4192 {
4193 m_reporter->Report(
4194 wxString::Format( _( "No valid PCB assembly; cannot create output file '%s'.\n" ), aFileName ),
4196 return false;
4197 }
4198
4200
4201 performMeshing( m_assy );
4202
4203 wxFileName fn( aFileName );
4204
4205 wxFileName u3dTmpfn = wxFileName::CreateTempFileName( "" );
4206 wxFileName pdfTmpfn = wxFileName::CreateTempFileName( "" );
4207
4208 U3D::WRITER writer( u3dTmpfn.GetFullPath().ToStdString() );
4209 bool success = writer.Perform( m_doc );
4210
4211 // PDF test
4212 std::unique_ptr<PDF_PLOTTER> plotter = std::make_unique<PDF_PLOTTER>();
4213
4214 plotter->SetColorMode( true );
4215 plotter->Set3DExport( true );
4216 plotter->SetCreator( wxT( "Mark's awesome 3d exporter" ) );
4217 KIGFX::PCB_RENDER_SETTINGS renderSettings;
4218 plotter->SetRenderSettings( &renderSettings );
4219
4220 if( !plotter->OpenFile( pdfTmpfn.GetFullPath() ) )
4221 {
4222 m_reporter->Report( wxString::Format( wxT( "Cannot open temporary file '%s'.\n" ), pdfTmpfn.GetFullPath() ),
4224 success = false;
4225 }
4226 else
4227 {
4228 plotter->StartPlot( "1", "3D Model" );
4229 double fov_degrees = 16.5f;
4230
4231 // kind of an arbitrary distance determination
4232 float distance = sqrt( writer.GetMeshBoundingBox().SquareExtent() ) * 3;
4233
4234 std::vector<PDF_3D_VIEW> views;
4235
4236 VECTOR3D camTarget = writer.GetCenter();
4237
4238
4239 std::vector<float> c2wMatrix =
4240 PDF_PLOTTER::CreateC2WMatrixFromAngles( camTarget, distance, 180.0f, -75.0f, 25.0f );
4241
4242 views.emplace_back( PDF_3D_VIEW{
4243 .m_name = "Default",
4244 .m_cameraMatrix = c2wMatrix,
4245 .m_cameraCenter = (float) distance,
4246 .m_fov = (float) fov_degrees,
4247 } );
4248
4249
4250
4251 c2wMatrix = PDF_PLOTTER::CreateC2WMatrixFromAngles( camTarget, distance, 180.0, 0.0f, 0.0f );
4252
4253 views.emplace_back( PDF_3D_VIEW{
4254 .m_name = "Top",
4255 .m_cameraMatrix = c2wMatrix,
4256 .m_cameraCenter = (float) distance,
4257 .m_fov = (float) fov_degrees,
4258 } );
4259
4260
4261
4262 c2wMatrix = PDF_PLOTTER::CreateC2WMatrixFromAngles( camTarget, distance, 0.0, 0.0f, 0.0f );
4263
4264 views.emplace_back( PDF_3D_VIEW{
4265 .m_name = "Bottom",
4266 .m_cameraMatrix = c2wMatrix,
4267 .m_cameraCenter = (float) distance,
4268 .m_fov = (float) fov_degrees,
4269 } );
4270
4271
4272
4273 c2wMatrix = PDF_PLOTTER::CreateC2WMatrixFromAngles( camTarget, distance, 90.0f, -90.0f, 90.0f );
4274
4275 views.emplace_back( PDF_3D_VIEW{
4276 .m_name = "Front",
4277 .m_cameraMatrix = c2wMatrix,
4278 .m_cameraCenter = (float) distance,
4279 .m_fov = (float) fov_degrees,
4280 } );
4281
4282 plotter->Plot3DModel( u3dTmpfn.GetFullPath(), views );
4283 plotter->EndPlot();
4284 }
4285
4286 if( success )
4287 {
4288 // Preserve the permissions of the current file
4289 KIPLATFORM::IO::DuplicatePermissions( fn.GetFullPath(), pdfTmpfn.GetFullPath() );
4290
4291 if( !wxRenameFile( pdfTmpfn.GetFullPath(), fn.GetFullPath(), true ) )
4292 {
4293 m_reporter->Report( wxString::Format( wxT( "Cannot rename temporary file '%s' to '%s'.\n" ),
4294 pdfTmpfn.GetFullPath(), fn.GetFullPath() ),
4296 success = false;
4297 }
4298 }
4299
4300 wxRemoveFile( u3dTmpfn.GetFullPath() );
4301
4302 return success;
4303}
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:106
int GetX() const
Definition board_item.h:100
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:440
bool IsOnLayer(PCB_LAYER_ID aLayer) const override
Test to see if this object is on the given layer.
Definition pad.h:908
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:2508
std::shared_ptr< SHAPE_SEGMENT > GetEffectiveHoleShape() const override
Return a SHAPE_SEGMENT object representing the pad's hole.
Definition pad.cpp:1063
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:52
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